Bree

Bree is a Node.js and JavaScript job task scheduler with worker threads, cr...

README

bree

build status code style styled with prettier made with lass license npm downloads
Bree is the best job scheduler for Node.js and JavaScript with cron, dates, ms, later, and human-friendly support.

Works in Node v12.17.0+, uses worker threads (Node.js) to spawn sandboxed processes, and supports async/await, retries, throttling, concurrency, and cancelable jobs with graceful shutdown. Simple, fast, and lightweight. Made for Forward Email and Lad.

Table of Contents



Foreword


Bree was created to give you fine-grained control with simplicity, and has built-in support for workers, sandboxed processes, graceful reloading, cron jobs, dates, human-friendly time representations, and much more.

We recommend you to query a persistent database in your jobs, to prevent specific operations from running more than once.

Bree does not force you to use an additional database layer of [Redis][] or [MongoDB][] to manage job state.

In doing so, you should manage boolean job states yourself using queries.  For instance, if you have to send a welcome email to users, only send a welcome email to users that do not have a Date value set yet for welcome_email_sent_at.

Install


[npm][]:

  1. ```sh
  2. npm install bree
  3. ```

[yarn][]:

  1. ```sh
  2. yarn add bree
  3. ```

Upgrading


To see details about upgrading from the last major version, please see UPGRADING.md.

IMPORTANT: Bree v9.0.0 has several breaking changes, please see UPGRADING.md for more insight.


NOTE: Bree v6.5.0 is the last version to support Node v10 and browsers.


Usage and Examples


The example below assumes that you have a directory jobs in the root of the directory from which you run this example.  For example, if the example below is at /path/to/script.js, then /path/to/jobs/ must also exist as a directory.  If you wish to disable this feature, then pass root: false as an option.

Inside this jobs directory are individual scripts which are run using [Workers][] per optional timeouts, and additionally, an optional interval or cron expression.  The example below contains comments, which help to clarify how this works.

The option jobs passed to a new instance of Bree (as shown below) is an Array.  It contains values which can either be a String (name of a job in the jobs directory, which is run on boot) OR it can be an Object with name, path, timeout, and interval properties.  If you do not supply a path, then the path is created using the root directory (defaults to jobs) in combination with the name.  If you do not supply values for timeout and/nor interval, then these values are defaulted to 0 (which is the default for both, see index.js for more insight into configurable default options).

We have also documented all Instance Options and Job Options in this README below.  Be sure to read those sections so you have a complete understanding of how Bree works.

ECMAScript modules (ESM)


  1. ``` js
  2. // app.mjs

  3. import Bree from 'bree';

  4. const bree = new Bree({
  5.   // ... (see below) ...
  6. });

  7. // top-level await supported in Node v14.8+
  8. await bree.start();

  9. // ... (see below) ...
  10. ```

Please reference the #CommonJS example below for more insight and options.

CommonJS (CJS)


  1. ``` js
  2. // app.js

  3. const path = require('path');

  4. // optional
  5. const ms = require('ms');
  6. const dayjs = require('dayjs');
  7. const Graceful = require('@ladjs/graceful');
  8. const Cabin = require('cabin');

  9. // required
  10. const Bree = require('bree');

  11. //
  12. // NOTE: see the "Instance Options" section below in this README
  13. // for the complete list of options and their defaults
  14. //
  15. const bree = new Bree({
  16.   //
  17.   // NOTE: by default the `logger` is set to `console`
  18.   // however we recommend you to use CabinJS as it
  19.   // will automatically add application and worker metadata
  20.   // to your log output, and also masks sensitive data for you
  21.   //
  22.   //
  23.   // NOTE: You can also pass `false` as `logger: false` to disable logging
  24.   //
  25.   logger: new Cabin(),

  26.   //
  27.   // NOTE: instead of passing this Array as an option
  28.   // you can create a `./jobs/index.js` file, exporting
  29.   // this exact same array as `module.exports = [ ... ]`
  30.   // doing so will allow you to keep your job configuration and the jobs
  31.   // themselves all in the same folder and very organized
  32.   //
  33.   // See the "Job Options" section below in this README
  34.   // for the complete list of job options and configurations
  35.   //
  36.   jobs: [
  37.     // runs `./jobs/foo.js` on start
  38.     'foo',

  39.     // runs `./jobs/foo-bar.js` on start
  40.     {
  41.       name: 'foo-bar'
  42.     },

  43.     // runs `./jobs/some-other-path.js` on start
  44.     {
  45.       name: 'beep',
  46.       path: path.join(__dirname, 'jobs', 'some-other-path')
  47.     },

  48.     // runs `./jobs/worker-1.js` on the last day of the month
  49.     {
  50.       name: 'worker-1',
  51.       interval: 'on the last day of the month'
  52.     },

  53.     // runs `./jobs/worker-2.js` every other day
  54.     {
  55.       name: 'worker-2',
  56.       interval: 'every 2 days'
  57.     },

  58.     // runs `./jobs/worker-3.js` at 10:15am and 5:15pm every day except on Tuesday
  59.     {
  60.       name: 'worker-3',
  61.       interval: 'at 10:15 am also at 5:15pm except on Tuesday'
  62.     },

  63.     // runs `./jobs/worker-4.js` at 10:15am every weekday
  64.     {
  65.       name: 'worker-4',
  66.       cron: '15 10 ? * *',
  67.       cronValidate: {
  68.         override: {
  69.           useBlankDay: true
  70.         }
  71.       }
  72.     },

  73.     // runs `./jobs/worker-5.js` on after 10 minutes have elapsed
  74.     {
  75.       name: 'worker-5',
  76.       timeout: '10m'
  77.     },

  78.     // runs `./jobs/worker-6.js` after 1 minute and every 5 minutes thereafter
  79.     {
  80.       name: 'worker-6',
  81.       timeout: '1m',
  82.       interval: '5m'
  83.       // this is unnecessary but shows you can pass a Number (ms)
  84.       // interval: ms('5m')
  85.     },

  86.     // runs `./jobs/worker-7.js` after 3 days and 4 hours
  87.     {
  88.       name: 'worker-7',
  89.       // this example uses `human-interval` parsing
  90.       timeout: '3 days and 4 hours'
  91.     },

  92.     // runs `./jobs/worker-8.js` at midnight (once)
  93.     {
  94.       name: 'worker-8',
  95.       timeout: 'at 12:00 am'
  96.     },

  97.     // runs `./jobs/worker-9.js` every day at midnight
  98.     {
  99.       name: 'worker-9',
  100.       interval: 'at 12:00 am'
  101.     },

  102.     // runs `./jobs/worker-10.js` at midnight on the 1st of every month
  103.     {
  104.       name: 'worker-10',
  105.       cron: '0 0 1 * *'
  106.     },

  107.     // runs `./jobs/worker-11.js` at midnight on the last day of month
  108.     {
  109.       name: 'worker-11',
  110.       cron: '0 0 L * *',
  111.       cronValidate: {
  112.         useLastDayOfMonth: true
  113.       }
  114.     },

  115.     // runs `./jobs/worker-12.js` at a specific Date (e.g. in 3 days)
  116.     {
  117.       name: 'worker-12',
  118.       //
  119.       date: dayjs().add(3, 'days').toDate()
  120.       // you can also use momentjs
  121.       //
  122.       // date: moment('1/1/20', 'M/D/YY').toDate()
  123.       // you can pass Date instances (if it's in the past it will not get run)
  124.       // date: new Date()
  125.     },

  126.     // runs `./jobs/worker-13.js` on start and every 2 minutes
  127.     {
  128.       name: 'worker-13',
  129.       interval: '2m'
  130.     },

  131.     // runs `./jobs/worker-14.js` on start with custom `new Worker` options (see below)
  132.     {
  133.       name: 'worker-14',
  134.       //
  135.       worker: {
  136.         workerData: {
  137.           foo: 'bar',
  138.           beep: 'boop'
  139.         }
  140.       }
  141.     },

  142.     // runs `./jobs/worker-15.js` **NOT** on start, but every 2 minutes
  143.     {
  144.       name: 'worker-15',
  145.       timeout: false, // <-- specify `false` here to prevent default timeout (e.g. on start)
  146.       interval: '2m'
  147.     },

  148.     // runs `./jobs/worker-16.js` on January 1st, 2022
  149.     // and at midnight on the 1st of every month thereafter
  150.     {
  151.       name: 'worker-16',
  152.       date: dayjs('1-1-2022', 'M-D-YYYY').toDate(),
  153.       cron: '0 0 1 * *'
  154.     }
  155.   ]
  156. });

  157. // handle graceful reloads, pm2 support, and events like SIGHUP, SIGINT, etc.
  158. const graceful = new Graceful({ brees: [bree] });
  159. graceful.listen();

  160. // start all jobs (this is the equivalent of reloading a crontab):
  161. (async () => {
  162.   await bree.start();
  163. })();

  164. /*
  165. // start only a specific job:
  166. (async () => {
  167.   await bree.start('foo');
  168. })();

  169. // stop all jobs
  170. bree.stop();

  171. // stop only a specific job:
  172. bree.stop('beep');

  173. // run all jobs (this does not abide by timeout/interval/cron and spawns workers immediately)
  174. bree.run();

  175. // run a specific job (...)
  176. bree.run('beep');

  177. (async () => {
  178.   // add a job array after initialization:
  179.   const added = await bree.add(['boop']); // will return array of added jobs
  180.   // this must then be started using one of the above methods

  181.   // add a job after initialization:
  182.   await bree.add('boop');
  183.   // this must then be started using one of the above methods
  184. })();

  185. // remove a job after initialization:
  186. bree.remove('boop');
  187. */
  188. ```

For more examples - including setting up bree with TypeScript, ESModules, and implementing an Email Queue, see the examples folder.

For a more complete demo using express see: Bree Express Demo

Instance Options


Here is the full list of options and their defaults.  See src/index.js for more insight if necessary.

PropertyTypeDefaultDescription
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
`logger`Object`console`This
`root`String`path.resolve('jobs')`Resolves
`silenceRootCheckError`Boolean`false`Silences
`doRootCheck`Boolean`true`Attempts
`removeCompleted`Boolean`false`Removes
`timeout`Number`0`Default
`interval`Number`0`Default
`jobs`Array`[]`Defaults
`hasSeconds`Boolean`false`This
`cronValidate`Object`{}`This
`closeWorkerAfterMs`Number`0`If
`defaultRootIndex`String`index.js`This
`defaultExtension`String`js`This
`acceptedExtensions`Array`['.js',This
`worker`Object`{}`These
`outputWorkerMetadata`Boolean`false`By
`errorHandler`Function`null`Set
`workerMessageHandler`Function`null`Set

Job Options


See Interval, Timeout, Date, and Cron Validate below for more insight besides this table:

PropertyTypeDescription
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
`name`StringThe
`path`StringThe
`timeout`Number,Sets
`interval`Number,Sets
`date`DateThis
`cron`StringA
`hasSeconds`BooleanOverrides
`cronValidate`ObjectOverrides
`closeWorkerAfterMs`NumberOverrides
`worker`ObjectOverrides
`outputWorkerMetadata`BooleanOverrides

Job Interval and Timeout Values


These values can include Number, Object, and String variable types:

Number values indicates the number of milliseconds for the timeout or interval
Object values must be a [later][] schedule object value (e.g. `later.parse.cron('15 10 ? *'))`)
String values can be either a [later][], [human-interval][], or [ms][] String values (e.g. [later][] supports Strings such as every 5 mins, [human-interval][] supports Strings such as 3 days and 4 hours, and [ms][] supports Strings such as 4h for four hours)

Listening for events


Bree extends from EventEmitter and emits two events:

worker created with an argument of name
worker deleted with an argument of name

If you'd like to know when your workers are created (or deleted), you can do so through this example:

  1. ``` js
  2. bree.on('worker created', (name) => {
  3.   console.log('worker created', name);
  4.   console.log(bree.workers.get(name));
  5. });

  6. bree.on('worker deleted', (name) => {
  7.   console.log('worker deleted', name);
  8.   console.log(!bree.worker.has(name));
  9. });
  10. ```

Custom error/message handling


If you'd like to override default behavior for worker error/message handling, provide a callback function as errorHandler or workerMessageHandler parameter when creating a Bree instance.

NOTE: Any console.log calls, from within the worker, will not be sent to stdout/stderr until the main thread is available. Furthermore, any console.log calls, from within the worker, will not be sent if the process is terminated before the message is printed. You should use parentPort.postMessage() alongside errorHandler or workerMessageHandler to print to stdout/stderr during worker execution. This is a known bug for workers.


An example use-case. If you want to call an external service to record an error (like Honeybadger, Sentry, etc.) along with logging the error internally. You can do so with:

  1. ``` js
  2. const logger = ('../path/to/logger');
  3. const errorService = ('../path/to/error-service');

  4. new Bree({
  5.   jobs: [
  6.     {
  7.       name: 'job that sometimes throws errors',
  8.       path: jobFunction
  9.     }
  10.   ],
  11.   errorHandler: (error, workerMetadata) => {
  12.     // workerMetadata will be populated with extended worker information only if
  13.     // Bree instance is initialized with parameter `workerMetadata: true`
  14.     if (workerMetadata.threadId) {
  15.       logger.info(`There was an error while running a worker ${workerMetadata.name} with thread ID: ${workerMetadata.threadId}`)
  16.     } else {
  17.       logger.info(`There was an error while running a worker ${workerMetadata.name}`)
  18.     }

  19.     logger.error(error);
  20.     errorService.captureException(error);
  21.   }
  22. });
  23. ```

Cancellation, Retries, Stalled Jobs, and Graceful Reloading


We recommend that you listen for "cancel" event in your worker paths.  Doing so will allow you to handle graceful cancellation of jobs.  For example, you could use [p-cancelable][]

Here's a quick example of how to do that (e.g. ./jobs/some-worker.js):

  1. ``` js
  2. //
  3. const { parentPort } = require('worker_threads');

  4. // ...

  5. function cancel() {
  6.   // do cleanup here
  7.   // (if you're using @ladjs/graceful, the max time this can run by default is 5s)

  8.   // send a message to the parent that we're ready to terminate
  9.   // (you could do `process.exit(0)` or `process.exit(1)` instead if desired
  10.   // but this is a bit of a cleaner approach for worker termination
  11.   if (parentPort) parentPort.postMessage('cancelled');
  12.   else process.exit(0);
  13. }

  14. if (parentPort)
  15.   parentPort.once('message', message => {
  16.     if (message === 'cancel') return cancel();
  17.   });
  18. ```

If you'd like jobs to retry, simply wrap your usage of promises with [p-retry][].

We leave it up to you to have as much fine-grained control as you wish.

See [@ladjs/graceful][lad-graceful] for more insight into how this package works.

Interval, Timeout, Date, and Cron Validation


If you need help writing cron expressions, you can reference crontab.guru.

We support [later][], [human-interval][], or [ms][] String values for both timeout and interval.

If you pass a cron property, then it is validated against [cron-validate][].

You can pass a Date as the date property, but you cannot combine both date and timeout.

If you do pass a Date, then it is only run if it is in the future.

See Job Interval and Timeout Values above for more insight.

Writing jobs with Promises and async-await


If jobs are running with Node pre-v14.8.0, which enables top-level async-await support, here is the working alternative:

  1. ``` js
  2. const { parentPort } = require('worker_threads');

  3. const delay = require('delay');
  4. const ms = require('ms');

  5. (async () => {
  6.   // wait for a promise to finish
  7.   await delay(ms('10s'));

  8.   // signal to parent that the job is done
  9.   if (parentPort) parentPort.postMessage('done');
  10.   else process.exit(0);
  11. })();
  12. ```

Callbacks, Done, and Completion States


To close out the worker and signal that it is done, you can simply parentPort.postMessage('done'); and/or process.exit(0).

While writing your jobs (which will run in [worker][workers] threads), you should do one of the following:

Signal to the main thread that the process has completed by sending a "done" message (per the example above in Writing jobs with Promises and async-await)
Exit the process if there is NOT an error with code 0 (e.g. process.exit(0);)
Throw an error if an error occurs (this will bubble up to the worker event error listener and terminate it)
Exit the process if there IS an error with code 1 (e.g. process.exit(1))

Long-running jobs


If a job is already running, a new worker thread will not be spawned, instead logger.error will be invoked with an error message (no error will be thrown, don't worry).  This is to prevent bad practices from being used.  If you need something to be run more than one time, then make the job itself run the task multiple times.  This approach gives you more fine-grained control.

By default, workers run indefinitely and are not closed until they exit (e.g. via process.exit(0) or process.exit(1), OR send to the parent port a "close" message, which will subsequently call worker.close() to close the worker thread.

If you wish to specify a maximum time (in milliseconds) that a worker can run, then pass closeWorkerAfterMs (Number) either as a default option when creating a new Bree() instance (e.g. new Bree({ closeWorkerAfterMs: ms('10s') })) or on a per-job configuration, e.g. { name: 'beep', closeWorkerAfterMs: ms('5m') }.

As of v6.0.0 when you pass closeWorkerAfterMs, the timer will start once the worker is signaled as "online" (as opposed to previous versions which did not take this into account).

Complex timeouts and intervals


Since we use [later][], you can pass an instance of later.parse.recur, later.parse.cron, or later.parse.text as the timeout or interval property values (e.g. if you need to construct something manually).

You can also use [dayjs][] to construct dates (e.g. from now or a certain date) to millisecond differences using dayjs().diff(new Date(), 'milliseconds').  You would then pass that returned Number value as timeout or interval as needed.

Custom Worker Options


You can pass a default worker configuration object as new Bree({ worker: { ... } });.

These options are passed to the options argument when we internally invoke new Worker(path, options).

Additionally, you can pass custom worker options on a per-job basis through a worker property Object on the job definition.

See complete documentation for options (but you usually don't have to modify these).

Using functions for jobs


It is highly recommended to use files instead of functions. However, sometimes it is necessary to use functions.

You can pass a function to be run as a job:

  1. ``` js
  2. new Bree({ jobs: [someFunction] });
  3. ```

(or)

  1. ``` js
  2. new Bree({
  3.   jobs: [
  4.     {
  5.       name: 'job with function',
  6.       path: someFunction
  7.     }
  8.   ]
  9. });
  10. ```

The function will be run as if it's in its own file, therefore no variables or dependencies will be shared from the local context by default.

You should be able to pass data via worker.workerData (see Custom Worker Options).

Note that you cannot pass a built-in nor bound function.

Typescript and Usage with Bundlers


When working with a bundler or a tool that transpiles your code in some form or another, we recommend that your bundler is set up in a way that transforms both your application code and your jobs. Because your jobs are in their own files and are run in their own separate threads, they will not be part of your applications dependency graph and need to be setup as their own entry points. You need to ensure you have configured your tool to bundle your jobs into a jobs folder and keep them properly relative to your entry point folder.

We recommend setting the root instance options to path.join(__dirname,'jobs') so that bree searches for your jobs folder relative to the file being ran. (by default it searches for jobs relative to where node is invoked). We recommend treating each job as an entry point and running all jobs through the same transformations as your app code.

After an example transformation - you should expect the output in your dist folder to look like:

  1. ```tree
  2. - dist
  3.   |-jobs
  4.     |-job.js
  5.   |-index.js
  6. ```

For some example TypeScript set ups - see the examples folder.

For another alternative also see the @breejs/ts-worker plugin.

Concurrency


We recommend using the following packages in your workers for handling concurrency:

* * * *

Plugins


Plugins can be added to Bree using a similar method to Day.js

To add a plugin use the following method:

  1. ``` js
  2. Bree.extend(plugin, options);
  3. ```

Available Plugins



Creating plugins for Bree


Plugins should be a function that recieves an options object and the Bree class:

  1. ``` js
  2.   const plugin = (options, Bree) => {
  3.     /* plugin logic */
  4.   };
  5. ```

Real-world usage


More detailed examples can be found in [Forward Email][forward-email], [Lad][], and [Ghost][ghost].

Contributors


NameWebsite
-------------------------------------------------
**Nick
**shadowgate15**

License



##

#

[ms]: https://github.com/vercel/ms

[human-interval]: https://github.com/agenda/human-interval

[npm]: https://www.npmjs.com/

[yarn]: https://yarnpkg.com/

[workers]: https://nodejs.org/api/worker_threads.html

[lad]: https://lad.js.org

[p-retry]: https://github.com/sindresorhus/p-retry

[p-cancelable]: https://github.com/sindresorhus/p-cancelable

[later]: https://breejs.github.io/later/parsers.html

[cron-validate]: https://github.com/Airfooox/cron-validate

[forward-email]: https://github.com/forwardemail/forwardemail.net

[dayjs]: https://github.com/iamkun/dayjs

[redis]: https://redis.io/

[mongodb]: https://www.mongodb.com/

[lad-graceful]: https://github.com/ladjs/graceful

[cabin]: https://cabinjs.com

[moment]: https://momentjs.com

[ghost]: https://ghost.org/