agenda 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/History.md CHANGED
@@ -1,12 +1,62 @@
1
1
  Next
2
2
  ====
3
3
 
4
- * https://github.com/agenda/agenda/pulls
4
+ * https://github.com/agenda/agenda/pulls
5
+
6
+ 3.1.0 / 2020-04-07
7
+ ==================
8
+
9
+ _Stay safe!_
10
+
11
+ * Fix for skipImmediate resetting nextRunAt to current date ([#860](https://github.com/agenda/agenda/pull/860)) (Thanks @AshlinDuncan!)
12
+ * Fix deprecated reconnect options ([#948](https://github.com/agenda/agenda/pull/948)) (Thanks @ekegodigital!)
13
+ * Add ability to set a skip when querying jobs. ([#898](https://github.com/agenda/agenda/pull/898)) (Thanks @cjolif!)
14
+
15
+ Internal:
16
+ * Fixed deprecated MongoDB functions in tests ([#928](https://github.com/agenda/agenda/pull/928)) (Thanks @MichielDeMey!)
17
+ * Updated devDependencies
18
+
19
+ Thank you @koresar, @sampathBlam, and @MichielDeMey helping to review PRs for this release! 👏
20
+
21
+
22
+ 3.0.0 / 2020-02-13
23
+ ==================
24
+
25
+ * Support MongoDB's Unified Topology Design ([#921](https://github.com/agenda/agenda/pull/921)) (Thanks @viktorzavadil!)
26
+ * Fix: check that the new nextRunAt is different that the previous nextRunAt ([#863](https://github.com/agenda/agenda/pull/863)) (Thanks @RaphaelRheault!)
27
+ * Update dependencies. Most notably MongoDB driver 3.4 → 3.5 ([#899](https://github.com/agenda/agenda/pull/899), [#900](https://github.com/agenda/agenda/pull/900), [#903](https://github.com/agenda/agenda/pull/903), [#906](https://github.com/agenda/agenda/pull/906), [#908](https://github.com/agenda/agenda/pull/908), [#910](https://github.com/agenda/agenda/pull/910), [#912](https://github.com/agenda/agenda/pull/912), [#913](https://github.com/agenda/agenda/pull/913), [#920](https://github.com/agenda/agenda/pull/920), [#922](https://github.com/agenda/agenda/pull/922))
28
+ * Documentation updates, thanks @MichielDeMey and @Sunghee2. ([#923](https://github.com/agenda/agenda/pull/923) & [#907](https://github.com/agenda/agenda/pull/907))
29
+
30
+ BREAKING
31
+ --------
32
+
33
+ * Stop testing for Node.js 8. This might still work but we're no longer actively testing for it. ([#925](https://github.com/agenda/agenda/pull/925))
34
+
35
+ 2.3.0 / 2019-12-16
36
+ ==================
37
+
38
+ * Improved performance in situations when there are many "expired" jobs in the database ([#869](https://github.com/agenda/agenda/pull/869)) (Thanks @mfred488!)
39
+ * Fix periodic node.js process unhandledRejection ([#887](https://github.com/agenda/agenda/pull/887)) (Thanks @koresar and @Scorpil)
40
+ * Update dependencies
41
+
42
+ 2.2.0 / 2019-11-24
43
+ ==================
44
+
45
+ * Fix `skipImmediate` option in `.every` ([#861](https://github.com/agenda/agenda/pull/861)) (Thanks @erics2783!)
46
+ * Add try/catch block to agenda#now method ([#876](https://github.com/agenda/agenda/pull/876)) (Thanks @sampathBlam!)
47
+ * Refactor job queuing mechanism. Agenda n ow guarantees priority when executing jobs scheduled the same datetime. Fixes also some tests. ([#852](https://github.com/agenda/agenda/pull/852)) (Thank you @dmbarreiro!)
48
+ * Update dependencies (Kudos @simison!)
49
+ Most notably `mongodb` ~3.2.7 -> ~3.3.0 ([changelog](https://github.com/mongodb/node-mongodb-native/tree/v3.3.0)) — highlights:
50
+ - Mongo DB Server Version 4.2 feature support
51
+ - Merged `mongodb-core` into `node-mongodb-native`
52
+ - Beta support for MongoDB Client-Side Encryption
53
+ - SRV Polling for Sharded Clusters
54
+ * Updates to documentation (Thank you @lautarobock, @sampathBlam, @indatawetrust)
5
55
 
6
56
  2.1.0 / 2019-09-09
7
57
  ==================
8
58
  * Support async functions in job processing ([#653](https://github.com/agenda/agenda/pull/653)) (thanks @princjef!)
9
- * Allow sorting and limiting jobs when searching ((#665)[https://github.com/agenda/agenda/pull/665]) (thank you @edwin-jones)
59
+ * Allow sorting and limiting jobs when searching ([#665](https://github.com/agenda/agenda/pull/665)) (thank you @edwin-jones)
10
60
  * Update MongoClient connection settings with `useNewUrlParser: true` to remove the deprecation warning. ([#806](https://github.com/agenda/agenda/pull/806)) (thanks @dpawson905!)
11
61
  * Allow valid date strings when scheduling ([#808](https://github.com/agenda/agenda/pull/808)) (Thanks @wingsbob!)
12
62
  * Update dependencies ([#820](https://github.com/agenda/agenda/pull/820))
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://cdn.rawgit.com/agenda/agenda/master/agenda.svg" alt="Agenda" width="100" height="100">
2
+ <img src="https://cdn.jsdelivr.net/gh/agenda/agenda@master/agenda.svg" alt="Agenda" width="100" height="100">
3
3
  </p>
4
4
  <p align="center">
5
5
  A light-weight job scheduling library for Node.js
@@ -25,6 +25,7 @@
25
25
  - Event backed job queue that you can hook into.
26
26
  - [Agendash](https://github.com/agenda/agendash): optional standalone web-interface.
27
27
  - [Agenda-rest](https://github.com/agenda/agenda-rest): optional standalone REST API.
28
+ - [inversify-agenda](https://github.com/lautarobock/inversify-agenda) - Some utilities for the development of agenda workers with Inversify
28
29
 
29
30
  ### Feature Comparison
30
31
 
@@ -48,7 +49,7 @@ Agenda is great if you need something that is simple and backed by MongoDB.
48
49
  | Persistence | ✓ | ✓ | ✓ |
49
50
  | UI | ✓ | | ✓ |
50
51
  | REST API | | | ✓ |
51
- | Optimized for | Jobs / Messages | Jobs | Messages | Jobs |
52
+ | Optimized for | Jobs / Messages | Messages | Jobs |
52
53
 
53
54
  _Kudos for making the comparison chart goes to [Bull](https://www.npmjs.com/package/bull#feature-comparison) maintainers._
54
55
 
@@ -416,10 +417,6 @@ under `job.attrs.data`.
416
417
  `options` is an optional argument that will be passed to [`job.repeatEvery`](#repeateveryinterval-options).
417
418
  In order to use this argument, `data` must also be specified.
418
419
 
419
- `cb` is an optional callback function which will be called when the job has been
420
- persisted in the database.
421
-
422
-
423
420
  Returns the `job`.
424
421
 
425
422
  ```js
@@ -449,9 +446,6 @@ Schedules a job to run `name` once at a given time. `when` can be a `Date` or a
449
446
  `data` is an optional argument that will be passed to the processing function
450
447
  under `job.attrs.data`.
451
448
 
452
- `cb` is an optional callback function which will be called when the job has been
453
- persisted in the database.
454
-
455
449
  Returns the `job`.
456
450
 
457
451
  ```js
@@ -493,37 +487,33 @@ console.log('Job successfully saved');
493
487
  ## Managing Jobs
494
488
 
495
489
 
496
- ### jobs(mongodb-native query, mongodb-native sort, mongodb-native limit)
490
+ ### jobs(mongodb-native query, mongodb-native sort, mongodb-native limit, mongodb-native skip)
497
491
 
498
- Lets you query (then sort and limit the result) all of the jobs in the agenda job's database. These are full [mongodb-native](https://github.com/mongodb/node-mongodb-native) `find`, `sort` and `limit` commands. See mongodb-native's documentation for details.
492
+ Lets you query (then sort, limit and skip the result) all of the jobs in the agenda job's database. These are full [mongodb-native](https://github.com/mongodb/node-mongodb-native) `find`, `sort`, `limit` and `skip` commands. See mongodb-native's documentation for details.
499
493
 
500
494
  ```js
501
- const jobs = await agenda.jobs({name: 'printAnalyticsReport'}, {data:-1}, 3);
495
+ const jobs = await agenda.jobs({name: 'printAnalyticsReport'}, {data:-1}, 3, 1);
502
496
  // Work with jobs (see below)
503
497
  ```
504
498
 
505
- ### cancel(mongodb-native query, cb)
499
+ ### cancel(mongodb-native query)
506
500
 
507
- Cancels any jobs matching the passed mongodb-native query, and removes them from the database.
501
+ Cancels any jobs matching the passed mongodb-native query, and removes them from the database. Returns a Promise resolving to the number of cancelled jobs, or rejecting on error.
508
502
 
509
503
  ```js
510
- agenda.cancel({name: 'printAnalyticsReport'}, (err, numRemoved) => {
511
- // ...
512
- });
504
+ const numRemoved = await agenda.cancel({name: 'printAnalyticsReport'});
513
505
  ```
514
506
 
515
507
  This functionality can also be achieved by first retrieving all the jobs from the database using `agenda.jobs()`, looping through the resulting array and calling `job.remove()` on each. It is however preferable to use `agenda.cancel()` for this use case, as this ensures the operation is atomic.
516
508
 
517
- ### purge(cb)
509
+ ### purge()
518
510
 
519
- Removes all jobs in the database without defined behaviors. Useful if you change a definition name and want to remove old jobs.
511
+ Removes all jobs in the database without defined behaviors. Useful if you change a definition name and want to remove old jobs. Returns a Promise resolving to the number of removed jobs, or rejecting on error.
520
512
 
521
513
  *IMPORTANT:* Do not run this before you finish defining all of your jobs. If you do, you will nuke your database of jobs.
522
514
 
523
515
  ```js
524
- agenda.purge((err, numRemoved) => {
525
- // ...
526
- });
516
+ const numRemoved = await agenda.purge();
527
517
  ```
528
518
 
529
519
  ## Starting the job processor
@@ -710,7 +700,7 @@ try {
710
700
  console.log('Successfully removed job from collection');
711
701
  } catch (e) {
712
702
  console.error('Error removing job from collection');
713
- });
703
+ }
714
704
  ```
715
705
 
716
706
  ### disable()
@@ -960,7 +950,7 @@ const agenda = new Agenda(connectionOpts);
960
950
  const jobTypes = process.env.JOB_TYPES ? process.env.JOB_TYPES.split(',') : [];
961
951
 
962
952
  jobTypes.forEach(type => {
963
- require('./lib/jobs/' + type)(agenda);
953
+ require('./jobs/' + type)(agenda);
964
954
  });
965
955
 
966
956
  if (jobTypes.length) {
@@ -15,8 +15,8 @@ module.exports = async function(query) {
15
15
  const {result} = await this._collection.deleteMany(query);
16
16
  debug('%s jobs cancelled', result.n);
17
17
  return result.n;
18
- } catch (err) {
18
+ } catch (error) {
19
19
  debug('error trying to delete jobs from MongoDB');
20
- throw err;
20
+ throw error;
21
21
  }
22
22
  };
@@ -25,8 +25,13 @@ module.exports = function(url, collection, options, cb) {
25
25
  url = 'mongodb://' + url;
26
26
  }
27
27
 
28
+ let reconnectOptions = {autoReconnect: true, reconnectTries: Number.MAX_SAFE_INTEGER, reconnectInterval: this._processEvery};
29
+ if (options && options.useUnifiedTopology && options.useUnifiedTopology === true) {
30
+ reconnectOptions = {};
31
+ }
32
+
28
33
  collection = collection || 'agendaJobs';
29
- options = Object.assign({autoReconnect: true, reconnectTries: Number.MAX_SAFE_INTEGER, reconnectInterval: this._processEvery, useNewUrlParser: true}, options);
34
+ options = {...reconnectOptions, ...options};
30
35
  MongoClient.connect(url, options, (error, client) => {
31
36
  if (error) {
32
37
  debug('error connecting to MongoDB using collection: [%s]', collection);
@@ -35,8 +40,10 @@ module.exports = function(url, collection, options, cb) {
35
40
  } else {
36
41
  throw error;
37
42
  }
43
+
38
44
  return;
39
45
  }
46
+
40
47
  debug('successful connection to MongoDB using collection: [%s]', collection);
41
48
  self._db = client;
42
49
  self._mdb = client.db();
@@ -16,6 +16,7 @@ module.exports = function(name, options, processor) {
16
16
  processor = options;
17
17
  options = {};
18
18
  }
19
+
19
20
  this._definitions[name] = {
20
21
  fn: processor,
21
22
  concurrency: options.concurrency || this._defaultConcurrency,
@@ -25,7 +25,6 @@ module.exports = async function(interval, names, data, options) {
25
25
 
26
26
  job.attrs.type = 'single';
27
27
  job.repeatEvery(interval, options);
28
- job.computeNextRunAt();
29
28
  await job.save();
30
29
 
31
30
  return job;
@@ -46,8 +45,8 @@ module.exports = async function(interval, names, data, options) {
46
45
  debug('every() -> all jobs created successfully');
47
46
 
48
47
  return Promise.all(jobs);
49
- } catch (err) { // @TODO: catch - ignore :O
50
- debug('every() -> error creating one or more of the jobs');
48
+ } catch (error) { // @TODO: catch - ignore :O
49
+ debug('every() -> error creating one or more of the jobs', error);
51
50
  }
52
51
  };
53
52
 
@@ -8,77 +8,69 @@ const {createJob} = require('../utils');
8
8
  * @function
9
9
  * @param {String} jobName name of job to try to lock
10
10
  * @param {Object} definition definition used to tell how job is run
11
- * @param {Function} cb called when job lock fails or passes
12
11
  * @access protected
13
12
  * @caller jobQueueFilling() only
14
- * @returns {undefined}
13
+ * @returns Promise
15
14
  */
16
- module.exports = function(jobName, definition, cb) {
15
+ module.exports = async function(jobName, definition) {
17
16
  const self = this;
18
17
  const now = new Date();
19
18
  const lockDeadline = new Date(Date.now().valueOf() - definition.lockLifetime);
20
- debug('_findAndLockNextJob(%s, [Function], cb)', jobName);
19
+ debug('_findAndLockNextJob(%s, [Function])', jobName);
21
20
 
22
21
  // Don't try and access MongoDB if we've lost connection to it.
23
22
  // Trying to resolve crash on Dev PC when it resumes from sleep. NOTE: Does this still happen?
24
23
  const s = this._mdb.s || this._mdb.db.s;
25
- if (s.topology.connections().length === 0) {
24
+ if (s.topology.connections && s.topology.connections().length === 0 && !this._mongoUseUnifiedTopology) {
26
25
  if (s.topology.autoReconnect && !s.topology.isDestroyed()) {
27
26
  // Continue processing but notify that Agenda has lost the connection
28
27
  debug('Missing MongoDB connection, not attempting to find and lock a job');
29
28
  self.emit('error', new Error('Lost MongoDB connection'));
30
- cb();
31
29
  } else {
32
30
  // No longer recoverable
33
31
  debug('topology.autoReconnect: %s, topology.isDestroyed(): %s', s.topology.autoReconnect, s.topology.isDestroyed());
34
- cb(new Error('MongoDB connection is not recoverable, application restart required'));
32
+ throw new Error('MongoDB connection is not recoverable, application restart required');
35
33
  }
36
34
  } else {
37
35
  // /**
38
36
  // * Query used to find job to run
39
- // * @type {{$or: [*]}}
37
+ // * @type {{$and: [*]}}
40
38
  // */
41
39
  const JOB_PROCESS_WHERE_QUERY = {
42
- $or: [{
40
+ $and: [{
43
41
  name: jobName,
44
- lockedAt: null,
45
- nextRunAt: {$lte: this._nextScanAt},
46
42
  disabled: {$ne: true}
47
43
  }, {
48
- name: jobName,
49
- lockedAt: {$exists: false},
50
- nextRunAt: {$lte: this._nextScanAt},
51
- disabled: {$ne: true}
52
- }, {
53
- name: jobName,
54
- lockedAt: {$lte: lockDeadline},
55
- disabled: {$ne: true}
44
+ $or: [{
45
+ lockedAt: {$eq: null},
46
+ nextRunAt: {$lte: this._nextScanAt}
47
+ }, {
48
+ lockedAt: {$lte: lockDeadline}
49
+ }]
56
50
  }]
57
51
  };
58
52
 
59
53
  /**
60
- * Query used to set a job as locked
61
- * @type {{$set: {lockedAt: Date}}}
62
- */
54
+ * Query used to set a job as locked
55
+ * @type {{$set: {lockedAt: Date}}}
56
+ */
63
57
  const JOB_PROCESS_SET_QUERY = {$set: {lockedAt: now}};
64
58
 
65
59
  /**
66
- * Query used to affect what gets returned
67
- * @type {{returnOriginal: boolean, sort: object}}
68
- */
60
+ * Query used to affect what gets returned
61
+ * @type {{returnOriginal: boolean, sort: object}}
62
+ */
69
63
  const JOB_RETURN_QUERY = {returnOriginal: false, sort: this._sort};
70
64
 
71
65
  // Find ONE and ONLY ONE job and set the 'lockedAt' time so that job begins to be processed
72
- this._collection.findOneAndUpdate(JOB_PROCESS_WHERE_QUERY, JOB_PROCESS_SET_QUERY, JOB_RETURN_QUERY, (err, result) => {
73
- let job;
74
- if (!err && result.value) {
75
- debug('found a job available to lock, creating a new job on Agenda with id [%s]', result.value._id);
76
- job = createJob(self, result.value);
77
- }
78
- if (err) {
79
- debug('error occurred when running query to find and lock job');
80
- }
81
- cb(err, job);
82
- });
66
+ const result = await this._collection.findOneAndUpdate(JOB_PROCESS_WHERE_QUERY, JOB_PROCESS_SET_QUERY, JOB_RETURN_QUERY);
67
+
68
+ let job;
69
+ if (result.value) {
70
+ debug('found a job available to lock, creating a new job on Agenda with id [%s]', result.value._id);
71
+ job = createJob(self, result.value);
72
+ }
73
+
74
+ return job;
83
75
  }
84
76
  };
@@ -1,5 +1,6 @@
1
1
  const {EventEmitter} = require('events');
2
2
  const humanInterval = require('human-interval');
3
+ const JobProcessingQueue = require('./job-processing-queue');
3
4
 
4
5
  /**
5
6
  * @class Agenda
@@ -22,6 +23,7 @@ const humanInterval = require('human-interval');
22
23
  * @property {Array} _jobsToLock
23
24
  */
24
25
  class Agenda extends EventEmitter {
26
+ // eslint-disable-next-line default-param-last
25
27
  constructor(config = {}, cb) {
26
28
  super();
27
29
 
@@ -38,10 +40,10 @@ class Agenda extends EventEmitter {
38
40
  this._definitions = {};
39
41
  this._runningJobs = [];
40
42
  this._lockedJobs = [];
41
- this._jobQueue = [];
43
+ this._jobQueue = new JobProcessingQueue();
42
44
  this._defaultLockLifetime = config.defaultLockLifetime || 10 * 60 * 1000; // 10 minute default lockLifetime
43
45
  this._sort = config.sort || {nextRunAt: 1, priority: -1};
44
- this._indices = Object.assign({name: 1}, this._sort, {priority: -1, lockedAt: 1, nextRunAt: 1, disabled: 1});
46
+ this._indices = {name: 1, ...this._sort, priority: -1, lockedAt: 1, nextRunAt: 1, disabled: 1};
45
47
 
46
48
  this._isLockingOnTheFly = false;
47
49
  this._jobsToLock = [];
@@ -49,6 +51,9 @@ class Agenda extends EventEmitter {
49
51
 
50
52
  if (config.mongo) {
51
53
  this.mongo(config.mongo, config.db ? config.db.collection : undefined, cb);
54
+ if (config.mongo.s && config.mongo.topology && config.mongo.topology.s) {
55
+ this._mongoUseUnifiedTopology = Boolean(config.mongo.topology.s.options.useUnifiedTopology);
56
+ }
52
57
  } else if (config.db) {
53
58
  this.database(config.db.address, config.db.collection, config.db.options, cb);
54
59
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @class
3
+ * @param {Object} args - Job Options
4
+ * @property {Object} agenda - The Agenda instance
5
+ * @property {Object} attrs
6
+ */
7
+ class JobProcessingQueue {
8
+ constructor() {
9
+ this._queue = [];
10
+ }
11
+
12
+ get length() {
13
+ return this._queue.length;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Pops and returns last queue element (next job to be processed) without checking concurrency.
19
+ * @returns {Job} Next Job to be processed
20
+ */
21
+ JobProcessingQueue.prototype.pop = function() {
22
+ return this._queue.pop();
23
+ };
24
+
25
+ /**
26
+ * Inserts job in first queue position
27
+ * @param {Job} job job to add to queue
28
+ * @returns {undefined}
29
+ */
30
+ JobProcessingQueue.prototype.push = function(job) {
31
+ this._queue.push(job);
32
+ };
33
+
34
+ /**
35
+ * Inserts job in queue where it will be order from left to right in decreasing
36
+ * order of nextRunAt and priority (in case of same nextRunAt), if all values
37
+ * are even the first jobs to be introduced will have priority
38
+ * @param {Job} job job to add to queue
39
+ * @returns {undefined}
40
+ */
41
+ JobProcessingQueue.prototype.insert = function(job) {
42
+ const matchIndex = this._queue.findIndex(element => {
43
+ if (element.attrs.nextRunAt.getTime() <= job.attrs.nextRunAt.getTime()) {
44
+ if (element.attrs.nextRunAt.getTime() === job.attrs.nextRunAt.getTime()) {
45
+ if (element.attrs.priority >= job.attrs.priority) {
46
+ return true;
47
+ }
48
+ } else {
49
+ return true;
50
+ }
51
+ }
52
+
53
+ return false;
54
+ });
55
+
56
+ if (matchIndex === -1) {
57
+ this._queue.push(job);
58
+ } else {
59
+ this._queue.splice(matchIndex, 0, job);
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Returns (does not pop, element remains in queue) first element (always from the right)
65
+ * that can be processed (not blocked by concurrency execution)
66
+ * @param {Object} agendaDefinitions job to add to queue
67
+ * @returns {Job} Next Job to be processed
68
+ */
69
+ JobProcessingQueue.prototype.returnNextConcurrencyFreeJob = function(agendaDefinitions) {
70
+ let next;
71
+ for (next = this._queue.length - 1; next > 0; next -= 1) {
72
+ const def = agendaDefinitions[this._queue[next].attrs.name];
73
+ if (def.concurrency > def.running) {
74
+ break;
75
+ }
76
+ }
77
+
78
+ return this._queue[next];
79
+ };
80
+
81
+ module.exports = JobProcessingQueue;
@@ -8,13 +8,15 @@ const {createJob} = require('../utils');
8
8
  * @param {Object} query object for MongoDB
9
9
  * @param {Object} sort object for MongoDB
10
10
  * @param {Number} limit number of documents to return from MongoDB
11
+ * @param {Number} number of documents to skip in MongoDB
11
12
  * @returns {Promise} resolves when fails or passes
12
13
  */
13
- module.exports = async function(query = {}, sort = {}, limit = 0) {
14
+ module.exports = async function(query = {}, sort = {}, limit = 0, skip = 0) {
14
15
  const result = await this._collection
15
16
  .find(query)
16
17
  .sort(sort)
17
18
  .limit(limit)
19
+ .skip(skip)
18
20
  .toArray();
19
21
 
20
22
  return result.map(job => createJob(this, job));
package/lib/agenda/now.js CHANGED
@@ -8,16 +8,21 @@ const noCallback = require('../no-callback');
8
8
  * @function
9
9
  * @param {String} name name of job to schedule
10
10
  * @param {Object} data data to pass to job
11
- * @returns {Job} new job instance created
11
+ * @returns {Promise} resolves with the new job instance created
12
12
  */
13
13
  module.exports = async function(name, data) {
14
- // eslint-disable-next-line prefer-rest-params
15
- noCallback(arguments, 2);
16
14
  debug('Agenda.now(%s, [Object])', name);
17
- const job = this.create(name, data);
15
+ try {
16
+ // eslint-disable-next-line prefer-rest-params
17
+ noCallback(arguments, 2);
18
+ const job = this.create(name, data);
18
19
 
19
- job.schedule(new Date());
20
- await job.save();
20
+ job.schedule(new Date());
21
+ await job.save();
21
22
 
22
- return job;
23
+ return job;
24
+ } catch (error) {
25
+ debug('error trying to create a job for this exact moment');
26
+ throw error;
27
+ }
23
28
  };
@@ -126,12 +126,13 @@ module.exports = async function(job) {
126
126
  const result = await this._collection.findOneAndUpdate(query, update, {upsert: true, returnOriginal: false});
127
127
  return processDbResult(job, result);
128
128
  }
129
+
129
130
  // If all else fails, the job does not exist yet so we just insert it into MongoDB
130
131
  debug('using default behavior, inserting new job via insertOne() with props that were set: \n%O', props);
131
132
  const result = await this._collection.insertOne(props);
132
133
  return processDbResult(job, result);
133
- } catch (err) {
134
+ } catch (error) {
134
135
  debug('processDbResult() received an error, job was not updated/created');
135
- throw err;
136
+ throw error;
136
137
  }
137
138
  };
@@ -40,9 +40,9 @@ module.exports = function(when, names, data) {
40
40
  const jobs = await Promise.all(names.map(name => createJob(when, name, data)));
41
41
  debug('Agenda.schedule()::createJobs() -> all jobs created successfully');
42
42
  return jobs;
43
- } catch (err) {
43
+ } catch (error) {
44
44
  debug('Agenda.schedule()::createJobs() -> error creating one or more of the jobs');
45
- throw err;
45
+ throw error;
46
46
  }
47
47
  };
48
48
 
@@ -14,6 +14,7 @@ module.exports = async function() {
14
14
  debug('Agenda.start was already called, ignoring');
15
15
  return this._ready;
16
16
  }
17
+
17
18
  await this._ready;
18
19
  debug('Agenda.start called, creating interval to call processJobs every [%dms]', this._processEvery);
19
20
  this._processInterval = setInterval(processJobs.bind(this), this._processEvery);
@@ -15,6 +15,7 @@ module.exports = function() {
15
15
  const interval = this.attrs.repeatInterval;
16
16
  const timezone = this.attrs.repeatTimezone;
17
17
  const {repeatAt} = this.attrs;
18
+ const previousNextRunAt = this.attrs.nextRunAt || new Date();
18
19
  this.attrs.nextRunAt = undefined;
19
20
 
20
21
  const dateForTimezone = date => {
@@ -22,6 +23,7 @@ module.exports = function() {
22
23
  if (timezone !== null) {
23
24
  date.tz(timezone);
24
25
  }
26
+
25
27
  return date;
26
28
  };
27
29
 
@@ -36,23 +38,26 @@ module.exports = function() {
36
38
  try {
37
39
  const cronTime = new CronTime(interval);
38
40
  let nextDate = cronTime._getNextDateFrom(lastRun);
39
- if (nextDate.valueOf() === lastRun.valueOf()) {
41
+ if (nextDate.valueOf() === lastRun.valueOf() || nextDate.valueOf() <= previousNextRunAt.valueOf()) {
40
42
  // Handle cronTime giving back the same date for the next run time
41
43
  nextDate = cronTime._getNextDateFrom(dateForTimezone(new Date(lastRun.valueOf() + 1000)));
42
44
  }
45
+
43
46
  this.attrs.nextRunAt = nextDate;
44
- debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString());
45
- } catch (e) {
47
+ debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, new Date(this.attrs.nextRunAt).toISOString());
48
+ // Either `xo` linter or Node.js 8 stumble on this line if it isn't just ignored
49
+ } catch (error) { // eslint-disable-line no-unused-vars
46
50
  // Nope, humanInterval then!
47
51
  try {
48
52
  if (!this.attrs.lastRunAt && humanInterval(interval)) {
49
53
  this.attrs.nextRunAt = lastRun.valueOf();
50
- debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString());
54
+ debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, new Date(this.attrs.nextRunAt).toISOString());
51
55
  } else {
52
56
  this.attrs.nextRunAt = lastRun.valueOf() + humanInterval(interval);
53
- debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString());
57
+ debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, new Date(this.attrs.nextRunAt).toISOString());
54
58
  }
55
- } catch (e) {}
59
+ // Either `xo` linter or Node.js 8 stumble on this line if it isn't just ignored
60
+ } catch (error) {} // eslint-disable-line no-unused-vars
56
61
  } finally {
57
62
  if (isNaN(this.attrs.nextRunAt)) {
58
63
  this.attrs.nextRunAt = undefined;
@@ -90,5 +95,6 @@ module.exports = function() {
90
95
  } else if (repeatAt) {
91
96
  computeFromRepeatAt.call(this);
92
97
  }
98
+
93
99
  return this;
94
100
  };
package/lib/job/fail.js CHANGED
@@ -12,6 +12,7 @@ module.exports = function(reason) {
12
12
  if (reason instanceof Error) {
13
13
  reason = reason.message;
14
14
  }
15
+
15
16
  this.attrs.failReason = reason;
16
17
  this.attrs.failCount = (this.attrs.failCount || 0) + 1;
17
18
  const now = new Date();
package/lib/job/index.js CHANGED
@@ -16,6 +16,7 @@ const parsePriority = priority => {
16
16
  if (typeof priority === 'number' || priority instanceof Number) {
17
17
  return priority;
18
18
  }
19
+
19
20
  return priorityMap[priority];
20
21
  };
21
22
 
@@ -13,11 +13,14 @@ module.exports = function() {
13
13
  if (!this.attrs.lastRunAt) {
14
14
  return false;
15
15
  }
16
+
16
17
  if (!this.attrs.lastFinishedAt) {
17
18
  return true;
18
19
  }
20
+
19
21
  if (this.attrs.lockedAt && this.attrs.lastRunAt.getTime() > this.attrs.lastFinishedAt.getTime()) {
20
22
  return true;
21
23
  }
24
+
22
25
  return false;
23
26
  };
@@ -18,6 +18,7 @@ const parsePriority = priority => {
18
18
  if (typeof priority === 'number' || priority instanceof Number) {
19
19
  return priority;
20
20
  }
21
+
21
22
  return priorityMap[priority];
22
23
  };
23
24
 
@@ -13,9 +13,13 @@ module.exports = function(interval, options) {
13
13
  this.attrs.repeatInterval = interval;
14
14
  this.attrs.repeatTimezone = options.timezone ? options.timezone : null;
15
15
  if (options.skipImmediate) {
16
- this.attrs.lastRunAt = new Date();
16
+ // Set the lastRunAt time to the nextRunAt so that the new nextRunAt will be computed in reference to the current value.
17
+ this.attrs.lastRunAt = this.attrs.nextRunAt || new Date();
17
18
  this.computeNextRunAt();
18
19
  this.attrs.lastRunAt = undefined;
20
+ } else {
21
+ this.computeNextRunAt();
19
22
  }
23
+
20
24
  return this;
21
25
  };
package/lib/job/run.js CHANGED
@@ -12,6 +12,8 @@ module.exports = function() {
12
12
  const {agenda} = self;
13
13
  const definition = agenda._definitions[self.attrs.name];
14
14
 
15
+ // @TODO: this lint issue should be looked into: https://eslint.org/docs/rules/no-async-promise-executor
16
+ // eslint-disable-next-line no-async-promise-executor
15
17
  return new Promise(async(resolve, reject) => {
16
18
  self.attrs.lastRunAt = new Date();
17
19
  debug('[%s:%s] setting lastRunAt to: %s', self.attrs.name, self.attrs._id, self.attrs.lastRunAt.toISOString());
@@ -24,6 +26,7 @@ module.exports = function() {
24
26
  if (finished) {
25
27
  return;
26
28
  }
29
+
27
30
  finished = true;
28
31
 
29
32
  if (err) {
@@ -34,9 +37,9 @@ module.exports = function() {
34
37
 
35
38
  self.attrs.lockedAt = null;
36
39
 
37
- await self.save().catch(err => {
40
+ await self.save().catch(error => {
38
41
  debug('[%s:%s] failed to be saved to MongoDB', self.attrs.name, self.attrs._id);
39
- reject(err);
42
+ reject(error);
40
43
  });
41
44
  debug('[%s:%s] was saved successfully to MongoDB', self.attrs.name, self.attrs._id);
42
45
 
@@ -49,6 +52,7 @@ module.exports = function() {
49
52
  agenda.emit('success:' + self.attrs.name, self);
50
53
  debug('[%s:%s] has succeeded', self.attrs.name, self.attrs._id);
51
54
  }
55
+
52
56
  agenda.emit('complete', self);
53
57
  agenda.emit('complete:' + self.attrs.name, self);
54
58
  debug('[%s:%s] job finished at [%s] and was unlocked', self.attrs.name, self.attrs._id, self.attrs.lastFinishedAt);
@@ -65,6 +69,7 @@ module.exports = function() {
65
69
  debug('[%s:%s] has no definition, can not run', self.attrs.name, self.attrs._id);
66
70
  throw new Error('Undefined job');
67
71
  }
72
+
68
73
  if (definition.fn.length === 2) {
69
74
  debug('[%s:%s] process function being called', self.attrs.name, self.attrs._id);
70
75
  await definition.fn(self, jobCallback);
@@ -73,9 +78,9 @@ module.exports = function() {
73
78
  await definition.fn(self);
74
79
  await jobCallback();
75
80
  }
76
- } catch (err) {
81
+ } catch (error) {
77
82
  debug('[%s:%s] unknown error occurred', self.attrs.name, self.attrs._id);
78
- await jobCallback(err);
83
+ await jobCallback(error);
79
84
  }
80
85
  });
81
86
  };
@@ -52,9 +52,11 @@ module.exports = function(extraJob) {
52
52
  if (self._lockLimit && self._lockLimit <= self._lockedJobs.length) {
53
53
  shouldLock = false;
54
54
  }
55
+
55
56
  if (jobDefinition.lockLimit && jobDefinition.lockLimit <= jobDefinition.locked) {
56
57
  shouldLock = false;
57
58
  }
59
+
58
60
  debug('job [%s] lock status: shouldLock = %s', name, shouldLock);
59
61
  return shouldLock;
60
62
  }
@@ -65,46 +67,13 @@ module.exports = function(extraJob) {
65
67
  * @param {boolean} inFront puts the job in front of queue if true
66
68
  * @returns {undefined}
67
69
  */
68
- function enqueueJobs(jobs, inFront) {
70
+ function enqueueJobs(jobs) {
69
71
  if (!Array.isArray(jobs)) {
70
72
  jobs = [jobs];
71
73
  }
72
74
 
73
75
  jobs.forEach(job => {
74
- let jobIndex;
75
- let start;
76
- let loopCondition;
77
- let endCondition;
78
- let inc;
79
-
80
- if (inFront) {
81
- start = jobQueue.length ? jobQueue.length - 1 : 0;
82
- inc = -1;
83
- loopCondition = function() {
84
- return jobIndex >= 0;
85
- };
86
- endCondition = function(queuedJob) {
87
- return !queuedJob || queuedJob.attrs.priority < job.attrs.priority;
88
- };
89
- } else {
90
- start = 0;
91
- inc = 1;
92
- loopCondition = function() {
93
- return jobIndex < jobQueue.length;
94
- };
95
- endCondition = function(queuedJob) {
96
- return queuedJob.attrs.priority >= job.attrs.priority;
97
- };
98
- }
99
-
100
- for (jobIndex = start; loopCondition(); jobIndex += inc) {
101
- if (endCondition(jobQueue[jobIndex])) {
102
- break;
103
- }
104
- }
105
-
106
- // Insert the job to the queue at its prioritized position for processing
107
- jobQueue.splice(jobIndex, 0, job);
76
+ jobQueue.insert(job);
108
77
  });
109
78
  }
110
79
 
@@ -114,7 +83,7 @@ module.exports = function(extraJob) {
114
83
  * We do this because sometimes jobs are scheduled but will be run before the next process time
115
84
  * @returns {undefined}
116
85
  */
117
- function lockOnTheFly() {
86
+ async function lockOnTheFly() {
118
87
  // Already running this? Return
119
88
  if (self._isLockingOnTheFly) {
120
89
  debug('lockOnTheFly() already running, returning');
@@ -158,26 +127,22 @@ module.exports = function(extraJob) {
158
127
  const options = {returnOriginal: false};
159
128
 
160
129
  // Lock the job in MongoDB!
161
- self._collection.findOneAndUpdate(criteria, update, options, (err, resp) => {
162
- if (err) {
163
- throw err;
164
- }
165
- // Did the "job" get locked? Create a job object and run
166
- if (resp.value) {
167
- const job = createJob(self, resp.value);
168
- debug('found job [%s] that can be locked on the fly', job.attrs.name);
169
- self._lockedJobs.push(job);
170
- definitions[job.attrs.name].locked++;
171
- enqueueJobs(job);
172
- jobProcessing();
173
- }
130
+ const resp = await self._collection.findOneAndUpdate(criteria, update, options);
131
+
132
+ if (resp.value) {
133
+ const job = createJob(self, resp.value);
134
+ debug('found job [%s] that can be locked on the fly', job.attrs.name);
135
+ self._lockedJobs.push(job);
136
+ definitions[job.attrs.name].locked++;
137
+ enqueueJobs(job);
138
+ jobProcessing();
139
+ }
174
140
 
175
- // Mark lock on fly is done for now
176
- self._isLockingOnTheFly = false;
141
+ // Mark lock on fly is done for now
142
+ self._isLockingOnTheFly = false;
177
143
 
178
- // Re-run in case anything is in the queue
179
- lockOnTheFly();
180
- });
144
+ // Re-run in case anything is in the queue
145
+ await lockOnTheFly();
181
146
  }
182
147
 
183
148
  /**
@@ -185,7 +150,7 @@ module.exports = function(extraJob) {
185
150
  * @param {String} name fill a queue with specific job name
186
151
  * @returns {undefined}
187
152
  */
188
- function jobQueueFilling(name) {
153
+ async function jobQueueFilling(name) {
189
154
  // Don't lock because of a limit we have set (lockLimit, etc)
190
155
  if (!shouldLock(name)) {
191
156
  debug('lock limit reached in queue filling for [%s]', name);
@@ -197,12 +162,8 @@ module.exports = function(extraJob) {
197
162
  self._nextScanAt = new Date(now.valueOf() + self._processEvery);
198
163
 
199
164
  // For this job name, find the next job to run and lock it!
200
- self._findAndLockNextJob(name, definitions[name], (err, job) => {
201
- if (err) {
202
- debug('[%s] job lock failed while filling queue', name);
203
- throw err;
204
- }
205
-
165
+ try {
166
+ const job = await self._findAndLockNextJob(name, definitions[name]);
206
167
  // Still have the job?
207
168
  // 1. Add it to lock list
208
169
  // 2. Add count of locked jobs
@@ -213,10 +174,12 @@ module.exports = function(extraJob) {
213
174
  self._lockedJobs.push(job);
214
175
  definitions[job.attrs.name].locked++;
215
176
  enqueueJobs(job);
216
- jobQueueFilling(name);
177
+ await jobQueueFilling(name);
217
178
  jobProcessing();
218
179
  }
219
- });
180
+ } catch (error) {
181
+ debug('[%s] job lock failed while filling queue', name, error);
182
+ }
220
183
  }
221
184
 
222
185
  /**
@@ -233,29 +196,19 @@ module.exports = function(extraJob) {
233
196
  const now = new Date();
234
197
 
235
198
  // Get the next job that is not blocked by concurrency
236
- let next;
237
- for (next = jobQueue.length - 1; next > 0; next -= 1) {
238
- const def = definitions[jobQueue[next].attrs.name];
239
- if (def.concurrency > def.running) {
240
- break;
241
- }
242
- }
243
-
244
- // We now have the job we are going to process and its definition
245
- const job = jobQueue.splice(next, 1)[0];
246
- const jobDefinition = definitions[job.attrs.name];
199
+ const job = jobQueue.returnNextConcurrencyFreeJob(definitions);
247
200
 
248
201
  debug('[%s:%s] about to process job', job.attrs.name, job.attrs._id);
249
202
 
250
203
  // If the 'nextRunAt' time is older than the current time, run the job
251
204
  // Otherwise, setTimeout that gets called at the time of 'nextRunAt'
252
- if (job.attrs.nextRunAt < now) {
205
+ if (job.attrs.nextRunAt <= now) {
253
206
  debug('[%s:%s] nextRunAt is in the past, run the job immediately', job.attrs.name, job.attrs._id);
254
207
  runOrRetry();
255
208
  } else {
256
209
  const runIn = job.attrs.nextRunAt - now;
257
210
  debug('[%s:%s] nextRunAt is in the future, calling setTimeout(%d)', job.attrs.name, job.attrs._id, runIn);
258
- setTimeout(runOrRetry, runIn);
211
+ setTimeout(jobProcessing, runIn);
259
212
  }
260
213
 
261
214
  /**
@@ -264,6 +217,8 @@ module.exports = function(extraJob) {
264
217
  */
265
218
  async function runOrRetry() {
266
219
  if (self._processInterval) {
220
+ const job = jobQueue.pop();
221
+ const jobDefinition = definitions[job.attrs.name];
267
222
  if (jobDefinition.concurrency > jobDefinition.running && self._runningJobs.length < self._maxConcurrency) {
268
223
  // Get the deadline of when the job is not supposed to go past for locking
269
224
  const lockDeadline = new Date(Date.now() - jobDefinition.lockLifetime);
@@ -287,15 +242,12 @@ module.exports = function(extraJob) {
287
242
  debug('[%s:%s] processing job', job.attrs.name, job.attrs._id);
288
243
 
289
244
  job.run()
290
- .catch(err => [err, job])
245
+ .catch(error => [error, job])
291
246
  .then(job => processJobResult(...Array.isArray(job) ? job : [null, job])); // eslint-disable-line promise/prefer-await-to-then
292
-
293
- // Re-run the loop to check for more jobs to process (locally)
294
- jobProcessing();
295
247
  } else {
296
248
  // Run the job immediately by putting it on the top of the queue
297
249
  debug('[%s:%s] concurrency preventing immediate run, pushing job to top of queue', job.attrs.name, job.attrs._id);
298
- enqueueJobs(job, true);
250
+ enqueueJobs(job);
299
251
  }
300
252
  }
301
253
  }
@@ -311,10 +263,11 @@ module.exports = function(extraJob) {
311
263
  if (err) {
312
264
  return job.agenda.emit('error', err);
313
265
  }
266
+
314
267
  const {name} = job.attrs;
315
268
 
316
269
  // Job isn't in running jobs so throw an error
317
- if (self._runningJobs.indexOf(job) === -1) {
270
+ if (!self._runningJobs.includes(job)) {
318
271
  debug('[%s] callback was called, job must have been marked as complete already', job.attrs._id);
319
272
  throw new Error('callback already called - job ' + name + ' already marked complete');
320
273
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "agenda",
3
- "version": "2.1.0",
3
+ "version": "3.1.0",
4
4
  "description": "Light weight job scheduler for Node.js",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "lib"
8
8
  ],
9
9
  "engines": {
10
- "node": ">=8.0.0"
10
+ "node": ">=10.0.0"
11
11
  },
12
12
  "scripts": {
13
13
  "test": "npm run lint && npm run mocha",
@@ -16,11 +16,13 @@
16
16
  "mocha-debug": "DEBUG=agenda:**,-agenda:internal:** mocha --reporter spec --timeout 8000 -b",
17
17
  "mocha-debug-internal": "DEBUG=agenda:internal:** mocha --reporter spec --timeout 8000 -b",
18
18
  "mocha-debug-all": "DEBUG=agenda:** mocha --reporter spec --timeout 8000 -b",
19
+ "docs": "jsdoc --configure .jsdoc.json --verbose"
20
+ },
21
+ "config": {
19
22
  "blanket": {
20
23
  "pattern": "lib",
21
24
  "data-cover-never": "node_modules"
22
- },
23
- "docs": "jsdoc --configure .jsdoc.json --verbose"
25
+ }
24
26
  },
25
27
  "repository": {
26
28
  "type": "git",
@@ -40,29 +42,38 @@
40
42
  "url": "https://github.com/agenda/agenda/issues"
41
43
  },
42
44
  "dependencies": {
43
- "cron": "~1.7.2",
45
+ "cron": "~1.8.0",
44
46
  "date.js": "~0.3.3",
45
47
  "debug": "~4.1.1",
46
- "human-interval": "~0.1.6",
47
- "moment-timezone": "~0.5.26",
48
- "mongodb": "~3.2.7"
48
+ "human-interval": "~1.0.0",
49
+ "moment-timezone": "~0.5.27",
50
+ "mongodb": "~3.5.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "blanket": "1.2.3",
52
- "coveralls": "3.0.6",
54
+ "coveralls": "3.0.11",
53
55
  "delay": "4.3.0",
54
- "eslint": "4.9.0",
56
+ "eslint": "6.8.0",
57
+ "eslint-config-xo": "0.27.2",
58
+ "eslint-plugin-ava": "10.2.0",
59
+ "eslint-plugin-eslint-comments": "3.1.2",
60
+ "eslint-plugin-import": "2.20.2",
61
+ "eslint-plugin-node": "11.1.0",
62
+ "eslint-plugin-unicorn": "16.1.1",
55
63
  "expect.js": "0.3.1",
56
- "jsdoc": "3.6.3",
64
+ "jsdoc": "3.6.4",
57
65
  "jsdoc-template": "https://github.com/braintree/jsdoc-template",
58
- "mocha": "6.1.4",
66
+ "mocha": "7.1.1",
59
67
  "mocha-lcov-reporter": "1.3.0",
60
68
  "q": "1.5.1",
61
- "sinon": "7.3.2",
62
- "xo": "0.21.1"
69
+ "sinon": "9.0.1",
70
+ "xo": "0.27.2"
63
71
  },
64
72
  "xo": {
65
73
  "space": 2,
74
+ "ignores": [
75
+ "docs/**"
76
+ ],
66
77
  "rules": {
67
78
  "space-before-function-paren": [
68
79
  "error",