UNPKG

@hokify/agenda

Version:

Light weight job scheduler for Node.js

281 lines 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JobDbRepository = void 0; const debug = require("debug"); const mongodb_1 = require("mongodb"); const hasMongoProtocol_1 = require("./utils/hasMongoProtocol"); const log = debug('agenda:db'); /** * @class */ class JobDbRepository { constructor(agenda, connectOptions) { this.agenda = agenda; this.connectOptions = connectOptions; this.connectOptions.sort = this.connectOptions.sort || { nextRunAt: 1, priority: -1 }; } async createConnection() { const { connectOptions } = this; if (this.hasDatabaseConfig(connectOptions)) { log('using database config', connectOptions); return this.database(connectOptions.db.address, connectOptions.db.options); } if (this.hasMongoConnection(connectOptions)) { log('using passed in mongo connection'); return connectOptions.mongo; } throw new Error('invalid db config, or db config not found'); } hasMongoConnection(connectOptions) { return !!(connectOptions === null || connectOptions === void 0 ? void 0 : connectOptions.mongo); } hasDatabaseConfig(connectOptions) { var _a; return !!((_a = connectOptions === null || connectOptions === void 0 ? void 0 : connectOptions.db) === null || _a === void 0 ? void 0 : _a.address); } async getJobById(id) { return this.collection.findOne({ _id: new mongodb_1.ObjectId(id) }); } async getJobs(query, sort = {}, limit = 0, skip = 0) { return this.collection.find(query).sort(sort).limit(limit).skip(skip).toArray(); } async removeJobs(query) { const result = await this.collection.deleteMany(query); return result.deletedCount || 0; } async getQueueSize() { return this.collection.countDocuments({ nextRunAt: { $lt: new Date() } }); } async unlockJob(job) { // only unlock jobs which are not currently processed (nextRunAT is not null) await this.collection.updateOne({ _id: job.attrs._id, nextRunAt: { $ne: null } }, { $unset: { lockedAt: true } }); } /** * Internal method to unlock jobs so that they can be re-run */ async unlockJobs(jobIds) { await this.collection.updateMany({ _id: { $in: jobIds }, nextRunAt: { $ne: null } }, { $unset: { lockedAt: true } }); } async lockJob(job) { // Query to run against collection to see if we need to lock it const criteria = { _id: job.attrs._id, name: job.attrs.name, lockedAt: null, nextRunAt: job.attrs.nextRunAt, disabled: { $ne: true } }; // Update / options for the MongoDB query const update = { $set: { lockedAt: new Date() } }; const options = { returnDocument: 'after', sort: this.connectOptions.sort }; // Lock the job in MongoDB! const resp = await this.collection.findOneAndUpdate(criteria, update, options); return (resp === null || resp === void 0 ? void 0 : resp.value) || undefined; } async getNextJobToRun(jobName, nextScanAt, lockDeadline, now = new Date()) { /** * Query used to find job to run */ const JOB_PROCESS_WHERE_QUERY = { name: jobName, disabled: { $ne: true }, $or: [ { lockedAt: { $eq: null }, nextRunAt: { $lte: nextScanAt } }, { lockedAt: { $lte: lockDeadline } } ] }; /** * Query used to set a job as locked */ const JOB_PROCESS_SET_QUERY = { $set: { lockedAt: now } }; /** * Query used to affect what gets returned */ const JOB_RETURN_QUERY = { returnDocument: 'after', sort: this.connectOptions.sort }; // Find ONE and ONLY ONE job and set the 'lockedAt' time so that job begins to be processed const result = await this.collection.findOneAndUpdate(JOB_PROCESS_WHERE_QUERY, JOB_PROCESS_SET_QUERY, JOB_RETURN_QUERY); return result.value || undefined; } async connect() { var _a; const db = await this.createConnection(); log('successful connection to MongoDB', db.options); const collection = ((_a = this.connectOptions.db) === null || _a === void 0 ? void 0 : _a.collection) || 'agendaJobs'; this.collection = db.collection(collection); if (log.enabled) { log(`connected with collection: ${collection}, collection size: ${typeof this.collection.estimatedDocumentCount === 'function' ? await this.collection.estimatedDocumentCount() : '?'}`); } if (this.connectOptions.ensureIndex) { log('attempting index creation'); try { const result = await this.collection.createIndex({ name: 1, ...this.connectOptions.sort, priority: -1, lockedAt: 1, nextRunAt: 1, disabled: 1 }, { name: 'findAndLockNextJobIndex' }); log('index succesfully created', result); } catch (error) { log('db index creation failed', error); throw error; } } this.agenda.emit('ready'); } async database(url, options) { let connectionString = url; if (!(0, hasMongoProtocol_1.hasMongoProtocol)(connectionString)) { connectionString = `mongodb://${connectionString}`; } const client = await mongodb_1.MongoClient.connect(connectionString, { ...options }); return client.db(); } processDbResult(job, res) { log('processDbResult() called with success, checking whether to process job immediately or not'); // We have a result from the above calls if (res) { // Grab ID and nextRunAt from MongoDB and store it as an attribute on Job job.attrs._id = res._id; job.attrs.nextRunAt = res.nextRunAt; // check if we should process the job immediately this.agenda.emit('processJob', job); } // Return the Job instance return job; } async saveJobState(job) { const id = job.attrs._id; const $set = { lockedAt: (job.attrs.lockedAt && new Date(job.attrs.lockedAt)) || undefined, nextRunAt: (job.attrs.nextRunAt && new Date(job.attrs.nextRunAt)) || undefined, lastRunAt: (job.attrs.lastRunAt && new Date(job.attrs.lastRunAt)) || undefined, progress: job.attrs.progress, failReason: job.attrs.failReason, failCount: job.attrs.failCount, failedAt: job.attrs.failedAt && new Date(job.attrs.failedAt), lastFinishedAt: (job.attrs.lastFinishedAt && new Date(job.attrs.lastFinishedAt)) || undefined }; log('[job %s] save job state: \n%O', id, $set); const result = await this.collection.updateOne({ _id: id, name: job.attrs.name }, { $set }); if (!result.acknowledged || result.matchedCount !== 1) { throw new Error(`job ${id} (name: ${job.attrs.name}) cannot be updated in the database, maybe it does not exist anymore?`); } } /** * Save the properties on a job to MongoDB * @name Agenda#saveJob * @function * @param {Job} job job to save into MongoDB * @returns {Promise} resolves when job is saved or errors */ async saveJob(job) { var _a, _b; try { log('attempting to save a job'); // Grab information needed to save job but that we don't want to persist in MongoDB const id = job.attrs._id; // Store job as JSON and remove props we don't want to store from object // _id, unique, uniqueOpts // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _id, unique, uniqueOpts, ...props } = { ...job.toJson(), // Store name of agenda queue as last modifier in job data lastModifiedBy: this.agenda.attrs.name }; log('[job %s] set job props: \n%O', id, props); // Grab current time and set default query options for MongoDB const now = new Date(); const protect = {}; let update = { $set: props }; log('current time stored as %s', now.toISOString()); // If the job already had an ID, then update the properties of the job // i.e, who last modified it, etc if (id) { // Update the job and process the resulting data' log('job already has _id, calling findOneAndUpdate() using _id as query'); const result = await this.collection.findOneAndUpdate({ _id: id, name: props.name }, update, { returnDocument: 'after' }); return this.processDbResult(job, result.value); } if (props.type === 'single') { // Job type set to 'single' so... log('job with type of "single" found'); // If the nextRunAt time is older than the current time, "protect" that property, meaning, don't change // a scheduled job's next run time! if (props.nextRunAt && props.nextRunAt <= now) { log('job has a scheduled nextRunAt time, protecting that field from upsert'); protect.nextRunAt = props.nextRunAt; delete props.nextRunAt; } // If we have things to protect, set them in MongoDB using $setOnInsert if (Object.keys(protect).length > 0) { update.$setOnInsert = protect; } // Try an upsert log(`calling findOneAndUpdate(${props.name}) with job name and type of "single" as query`, await this.collection.findOne({ name: props.name, type: 'single' })); // this call ensure a job of this name can only exists once const result = await this.collection.findOneAndUpdate({ name: props.name, type: 'single' }, update, { upsert: true, returnDocument: 'after' }); log(`findOneAndUpdate(${props.name}) with type "single" ${((_a = result.lastErrorObject) === null || _a === void 0 ? void 0 : _a.updatedExisting) ? 'updated existing entry' : 'inserted new entry'}`); return this.processDbResult(job, result.value); } if (job.attrs.unique) { // If we want the job to be unique, then we can upsert based on the 'unique' query object that was passed in const query = job.attrs.unique; query.name = props.name; if ((_b = job.attrs.uniqueOpts) === null || _b === void 0 ? void 0 : _b.insertOnly) { update = { $setOnInsert: props }; } // Use the 'unique' query object to find an existing job or create a new one log('calling findOneAndUpdate() with unique object as query: \n%O', query); const result = await this.collection.findOneAndUpdate(query, update, { upsert: true, returnDocument: 'after' }); return this.processDbResult(job, result.value); } // If all else fails, the job does not exist yet so we just insert it into MongoDB log('using default behavior, inserting new job via insertOne() with props that were set: \n%O', props); const result = await this.collection.insertOne(props); return this.processDbResult(job, { _id: result.insertedId, ...props }); } catch (error) { log('processDbResult() received an error, job was not updated/created'); throw error; } } } exports.JobDbRepository = JobDbRepository; //# sourceMappingURL=JobDbRepository.js.map