quave:synced-cron
A simple cron system for Meteor. It supports syncronizing jobs between multiple processes. In other words, if you add a job that runs every hour and your deployment consists of multiple app servers, only one of the app servers will execute the job each time (whichever tries first).
Quave version is compatible with Meteor 2.12 and forward.
Installation
$ meteor add quave:synced-cron
API
Basics
To write a cron job, give it a unique name, a schedule and a function to run like below. SyncedCron uses the fantastic later.js library behind the scenes. A Later.js parse object is passed into the schedule call that gives you a huge amount of flexibility for scheduling your jobs, see the documentation.
1SyncedCron.add({ 2 name: 'Crunch some important numbers for the marketing department', 3 schedule: function(parser) { 4 // Note that the schedule function should not return 5 // a promise. It works only with synchronous functions. 6 7 // parser is a later.parse object 8 return parser.text('every 2 hours'); 9 }, 10 job: async function() { 11 await crushSomeNumbers(); 12 } 13});
You can also optionally provide the following functions:
onSuccess(opts): Called after the job is finished successfully and persisted. It receives the following props inside an object:output: The result returned by the job function.name: A string containing the name of the job.intendedAt: The Date object representing the intended execution time of the job.
onError(opts): Called when the job function throws an error and after it is persisted. It receives the following props inside an object:error: The error object.name: A string containing the name of the job.intendedAt: The Date object representing the intended execution time of the job.
allowParallelExecution: A boolean option that allows the same job to run in parallel if set totrue. Default isfalse.timeoutToConsiderRunningForParallelExecution: A number in milliseconds. If the job takes more time than this value and it's not finished, another instance of the job can be run in parallel. This option is only considered whenallowParallelExecutionisfalse.
To start processing your jobs, somewhere in your project add:
1SyncedCron.start();
Advanced
SyncedCron uses a collection called cronHistory to syncronize between processes. This also serves as a useful log of when jobs ran along with their output or error. A sample item looks like:
1{ _id: 'wdYLPBZp5zzbwdfYj', 2 intendedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), 3 finishedAt: Sun Apr 13 2014 17:34:01 GMT-0700 (MST), 4 name: 'Crunch some important numbers for the marketing department', 5 startedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), 6 result: '1982 numbers crunched' 7}
Call SyncedCron.nextScheduledAtDate(jobName) to find the date that the job
referenced by jobName will run next.
Call SyncedCron.remove(jobName) to remove and stop running the job referenced by jobName.
Call SyncedCron.stop() to remove and stop all jobs.
Call SyncedCron.pause() to stop all jobs without removing them. The existing jobs can be rescheduled (i.e. restarted) with SyncedCron.start().
To schedule a once off (i.e not recurring) event, create a job with a schedule like this parser.recur().on(date).fullDate();
Configuration
You can configure SyncedCron with the config method. Defaults are:
1 SyncedCron.config({ 2 // Log job run details to console 3 log: true, 4 5 // Use a custom logger function (defaults to Meteor's logging package) 6 logger: null, 7 8 // Name of collection to use for synchronisation and logging 9 collectionName: 'cronHistory', 10 11 // Default to using localTime 12 utc: false, 13 14 /* 15 TTL in seconds for history records in collection to expire 16 NOTE: Unset to remove expiry but ensure you remove the index from 17 mongo by hand 18 19 ALSO: SyncedCron can't use the `_ensureIndex` command to modify 20 the TTL index. The best way to modify the default value of 21 `collectionTTL` is to remove the index by hand (in the mongo shell 22 run `db.cronHistory.dropIndex({startedAt: 1})`) and re-run your 23 project. SyncedCron will recreate the index with the updated TTL. 24 */ 25 collectionTTL: 172800, 26 27 /* 28 Timeout in milliseconds to consider a job "blocked" (default: 30 minutes). 29 Jobs without finishedAt older than this will be marked as blocked. 30 This is used ONLY for startup cleanup of jobs from crashed processes. 31 Note: For parallel execution timeouts, use timeoutToConsiderRunningForParallelExecution 32 per-job instead. 33 Set to null or 0 to disable startup cleanup. 34 */ 35 blockedJobTimeoutMs: 30 * 60 * 1000, 36 37 /* 38 Whether to cleanup blocked jobs from other crashed processes on startup. 39 When enabled, jobs from other processes that have been running longer 40 than blockedJobTimeoutMs will be marked as terminated on startup. 41 Default: true 42 */ 43 cleanupBlockedJobsOnStartup: true 44 });
Blocked Jobs Cleanup
SyncedCron automatically handles "blocked" jobs - jobs that never received a finishedAt timestamp, usually due to server crashes or unexpected terminations.
Automatic cleanup on startup: When cleanupBlockedJobsOnStartup is enabled (default), SyncedCron will automatically mark blocked jobs from OTHER crashed processes as terminated when the server starts. Jobs from the current process are never affected.
Graceful shutdown: SyncedCron automatically handles SIGTERM, SIGINT, uncaughtException, and unhandledRejection signals to mark running jobs from the current process as terminated before shutdown.
The cleanup adds a terminatedBy field to identify how the job was terminated:
SIGTERM/SIGINT: Graceful shutdown signalUNCAUGHT_EXCEPTION/UNHANDLED_REJECTION: Fatal errorBLOCKED_ON_STARTUP: Cleaned up when server started
Logging
SyncedCron uses Meteor's logging package by default. If you want to use your own logger (for sending to other consumers or similar) you can do so by configuring the logger option.
SyncedCron expects a function as logger, and will pass arguments to it for you to take action on.
1const MyLogger = function(opts) { 2 console.log('Level', opts.level); 3 console.log('Message', opts.message); 4 console.log('Tag', opts.tag); 5} 6 7SyncedCron.config({ 8 logger: MyLogger 9}); 10 11SyncedCron.add({ name: 'Test Job', ... }); 12SyncedCron.start();
The opts object passed to MyLogger above includes level, message, and tag.
levelwill be one ofinfo,warn,error,debug.messageis something likeScheduled "Test Job" next run @Fri Mar 13 2015 10:15:00 GMT+0100 (CET).tagwill always be"SyncedCron"(handy for filtering).
Caveats
Beware, SyncedCron probably won't work as expected on certain shared hosting providers that shutdown app instances when they aren't receiving requests (like Heroku's free dyno tier or Meteor free galaxy).
Contributing
Write some code. Write some tests. To run the tests, do:
$ meteor test-packages ./