The best way to make a cronjob equivalent in Node.js

Sometimes, a normal cronjob is not what you are looking for. Maybe it's because you have to run tasks on an interval smaller than 1 minute. Or maybe you want each executions to be separated with equal delay starting when the previous execution finishes. Or maybe you just want each intervals to run in the same global context of execution.

A good pick for that is node.js, but be aware ! There is many packages out there implementing some kind of "sleep" mechanism that ends up using 100% CPU power during that supposedly "sleep" period... I've tried many of them and none of them have worked.

In the end, a simple loop made out of timeouts seems to work best and is apparently not causing any memory leaks in the long run ! Here is some exemples of it :

Equal delay cronjob

This exemple demonstrates how to make a "cronjob" that adds equal delay between each iterations. It's useful when you want to prevent overlaps of tasks for which completion's time is unknown.

equal-delay-cronjob.js

var async = require('async');

var RUNNING = true; // if the loop should continue  
var DELAY = 3000; // time (milliseconds) between each loop iterations  
var TIMEOUT = null;

// THE LOOP
function loop(){  
  TIMEOUT = null;
  console.log('Iteration started...');

  async.series([
    // STEP: task #1
    function (next){
      console.log('Task #1 done instantly.');
      next();
    },

    // STEP: task #2
    function (next){
      console.log('Task #2 in progress...');
      setTimeout(function(){
        console.log('Task #2 done after 1 seconds.');
        next();
      }, 1000);
    }
  ],

  // CALLBACK
  function(err){
    console.log('Iteration finished.');

    if(RUNNING){
      TIMEOUT = setTimeout(loop, DELAY);
    } else {
      process.exit();
    }
  });
}

function endLoop(){  
  if(TIMEOUT){
    clearTimeout(TIMEOUT);
    console.log('Loop is between two iterations so we are ending it now...');
    process.exit();
  } else {
    RUNNING = false;
    console.log('Loop will end after the current iteration...');
  }
}

// Handle interrupts
process.on('SIGINT', endLoop);  
process.on('SIGTERM', endLoop);  
process.on('SIGHUP', endLoop);

// Handle process exit
process.on('exit', function(){  
  console.log('Exiting loop.')
});

// start the asynchronous loop
loop();  

It's basically just a series of timeouts. I used async to demonstrate how to handle multiple separated tasks in a single iteration but you don't have to use it.

Classic cronjob

Well, when you want something closer to a normal cronjob you could just use intervals instead :

classic-cronjob.js

var RUNNING = true; // if the loop should continue  
var DELAY = 3000; // time (milliseconds) between each loop iterations  
var INTERVAL = null;

// THE LOOP
function loop(){  
  console.log('-- Iteration started : '+(new Date()));
  console.log('Task #1 done instantly.');

  console.log('Task #2 in progress (but it surely won\'t slow down the next iteration)...');
  setTimeout(function(){
    console.log('Task #2 done after 2 seconds.');
  }, 2000);
}

function endLoop(){  
  clearInterval(INTERVAL);
  process.exit();
}

// Handle interrupts
process.on('SIGINT', endLoop);  
process.on('SIGTERM', endLoop);  
process.on('SIGHUP', endLoop);

// Handle process exit
process.on('exit', function(){  
  console.log('Exiting loop.')
});

// start the asynchronous loop
loop();  
INTERVAL = setInterval(loop, DELAY);  

Note that the process events listeners at the end are useful to stop the loop from running when you try to stop it with CTRL+C for exemple.

Push it further

All of these exemples run each iterations in the same node.js process. Therefore, you could add a dnode server into that process to send infos about the state of the cronjob outside of it's process. This means that if you have a huge cronjob taking minutes to complete, you could bind that to a web API that returns the percentage of completion when it's running, or the time left before the next iteration when it's not.

So much possibilities and flexibility !