Javascript / Nodejs setInterval with scheduled start, simple cron style

4

For JavaScript and NodeJS, there are n libraries that are robust in Cron style, such as node-cron . But they are complex for simple situations, they are heavy to download in the browser or require additional dependency on NodeJS, which makes them impractical in simpler cases.

I want to create a function that can:

  • Accept the second, minute, and hour when the routine is ready to provide new data.
  • Check what hours are now on the client, and schedule the start of setInterval for the first time the server has new data.
  • Set the interval of setInterval to exactly the period between server updates.
  • Run in NodeJS environment and modern browsers and in IE8. If you do not know how to test in NodeJS, I'll test it for you .
  • There should be no additional dependency. No jQuery or NodeJS package.
  • The code should accept a type interval parameter, try again in x seconds , and pass a callback to the executed function so that if it returns exactly false , it will try again until it returns true or arrive at the time of the next standard execution. It considers that the server may fail and always return error, but the client should avoid overlapping additional attempts!

Real Use Example

The code below is responsible for synchronizing a table from a database with the browser or task NodeJS run

/**
 * Sincroniza o cliente com dados do banco de dados. Caso os dados sejam
 * diferentes dos que o cliente já possuia antes, passa ao callback true
 * do contrário, se percebeu que o servidor ainda não atualizou os dados
 * retorna false
 *
 * @param   {Function} [cb]  Callback. Opcional
 */
function sincronizar(cb) {
  var conseguiuSincronizar = false;
  // Executa uma rotina de sincronização de dados
  cb && cb(conseguiuSincronizar);
}

However, the database is only updated once every 15 minutes, that is, in minutes 0 , 15 , 30 , 45 time.

As saving to database may take some time, this routine would have to run every 15min and a few seconds of delay, for example, every 15min5s .

The problem when using setInterval is that to update every 15min, runs the risk of when the browser or the NodeJS task is initialized, there is a delay between the time when the client could obtain new information and the time where it is available. Setting setInterval over a period of less than 15min would cause data loss.

Bootstrap

Below is a bootstrap of how the simplest example could be made.

function cron (cb, s, m, h) {
    var start = 0, interval = 0;

    /* Logica para calculo de start e interval aqui */ 

    setTimeout(function () {
        setInterval(cb, interval);
    }, start);
}
    
asked by anonymous 09.02.2014 / 13:25

3 answers

5
function cron(callback, startTime, interval, threshold) {
    function callbackWithTimeout() {
        var timeout = interval === undefined ? null : setTimeout(callbackWithTimeout, interval);
        callback(timeout);
    }
    if (startTime === undefined) {
        // Corre em intervalos a partir do próximo ciclo de eventos
        return setTimeout(callbackWithTimeout, 0);
    }
    // Limitar startTime a hora de um dia
    startTime %= 86400000;
    var currentTime = new Date() % 86400000;
    if (interval === undefined) {
        // Corre uma vez
        // Se startTime é no passado, corre no próximo ciclo de eventos
        // Senão, espera o tempo suficiente
        return setTimeout(callbackWithTimeout, Math.max(0, startTime - currentTime));
    }
    else {
        var firstInterval = (startTime - currentTime) % interval;
        if (firstInterval < 0) firstInterval += interval;
        // Se falta mais do que threshold para a próxima hora,
        // corre no próximo ciclo de eventos, agenda para a próxima hora
        // e depois continua em intervalos
        if (threshold === undefined || firstInterval > threshold) {
            return setTimeout(function () {
                var timeout = setTimeout(callbackWithTimeout, firstInterval);
                callback(timeout);
            }, 0);
        }
        // Senão, começa apenas na próxima hora e continua em intervalos
        return setTimeout(callbackWithTimeout, firstInterval);
    }
}

Usage:

// Começar às 00:05:30 em intervalos de 00:15:00,
// mas não correr já se só faltar 00:00:30
// 
// Portanto, nas seguintes horas:
// 00:05:30 00:20:30 00:35:30 00:50:30
// 01:05:30 01:20:30 01:35:30 01:50:30
// 02:05:30 02:20:30 02:35:30 02:50:30
// 03:05:30 03:20:30 03:35:30 03:50:30
// ...
// 23:05:30 23:20:30 23:35:30 23:50:30
// 
// Se a hora actual é 12:00:00, começa já e depois às 12:05:30
// Se a hora actual é 12:05:00, começa só às 12:05:30
cron(function (timeout) { /* ... */ },
     (( 0*60 +  5)*60 + 30)*1000,
     (( 0*60 + 15)*60 +  0)*1000,
     (( 0*60 +  0)*60 + 30)*1000);

// Uma vez apenas às 12:05:30
cron(function (timeout) { /* ... */ },
     ((12*60 +  5)*60 +  0)*1000);

// Repetidamente em intervalos de 00:15:00
cron(function (timeout) { /* ... */ },
     undefined,
     (( 0*60 + 15)*60 +  0)*1000);

The function returns the value of setTimeout to be able to cancel before it starts, and the callback receives the value of the new setTimeout when there is repetition, in order to cancel the middle. For example, to run 4 times:

var count = 0;
cron(function (timeout) {
         count++;
         if (count == 4) clearTimeout(timeout);
     },
     (( 0*60 +  5)*60 + 30)*1000,
     (( 0*60 + 15)*60 +  0)*1000,
     (( 0*60 +  0)*60 + 30)*1000);
    
18.02.2014 / 01:38
3

A simple cron script, I just made some corrections to the original code to work.

/*  Copyright (C) 2009 Elijah Rutschman

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details, available at
    <http://www.gnu.org/licenses/>.
/*

/*
a typical cron entry has either wildcards (*) or an integer:

 .---------------- minute (0 - 59) 
 |  .------------- hour (0 - 23)
 |  |  .---------- day of month (1 - 31)
 |  |  |  .------- month (1 - 12)
 |  |  |  |  .---- day of week (0 - 6) (Sunday=0)
 |  |  |  |  |
 *  *  *  *  *

*/

var Cron = {
 "jobs" : [],
 "process" : function process() {
  var now = new Date();
  for (var i=0; i<Cron.jobs.length; i++) {
   if ( Cron.jobs[i].minute == "*" || parseInt(Cron.jobs[i].minute) == now.getMinutes() )
    if ( Cron.jobs[i].hour == "*" || parseInt(Cron.jobs[i].hour) == now.getHours() )
     if ( Cron.jobs[i].date == "*" || parseInt(Cron.jobs[i].date) == now.getDate() )
      if ( Cron.jobs[i].month == "*" || (parseInt(Cron.jobs[i].month) - 1) == now.getMonth() )
       if ( Cron.jobs[i].day == "*" || parseInt(Cron.jobs[i].day) == now.getDay() )
        Cron.jobs[i].run();
  }
  now = null;
  return process;
 },
 "id" : 0,
 "start" : function() {
  Cron.stop();
  Cron.id = setInterval(Cron.process(),60000);
 },
 "stop" : function() {
  clearInterval(Cron.id);

 },
 "Job" : function(cronstring, fun) {
  var _Job = this;
  var items = cronstring.match(/^([0-9]+|\*{1})[ \n\t\b]+([0-9]+|\*{1})[ \n\t\b]+([0-9]+|\*{1})[ \n\t\b]+([0-9]+|\*{1})[ \n\t\b]+([0-9]+|\*{1})[ \n\t\b]*$/);
  _Job.minute = items[1];
  _Job.hour = items[2];
  _Job.date = items[3];
  _Job.month = items[4];
  _Job.day = items[5];
  _Job.run = fun;
  Cron.jobs.push(_Job);
  _Job = null;
  items = null;
 }
}

Example usage:

// queue up some jobs to run
var j1 = new Cron.Job("* * * * *", function(){alert('cron job 1 just ran')})
var j2 = new Cron.Job("5 * * * *", function(){alert('cron job 2 just ran')})
var j3 = new Cron.Job("15 * * * *", function(){alert('cron job 3 just ran')})
var j4 = new Cron.Job("30 * * * *", function(){alert('cron job 4 just ran')})
Cron.start();

// Cron already running, but we can add more jobs, no problem
var j5 = new Cron.Job("0 * * * *", function(){alert('cron job 5 just ran')})

In case this script ignores the seconds, because even if you will not synchronize with server, it will probably have value difference, but would be simple to add. runs every minute to check if there is any script in the cron list to run.

It is easy to add and remove routines to be executed in the process list. Start and stop cron as a whole.

Example to run every 5 minutes:

var meusJobs = [];
for (var i = 0; i <= 60; i = i + 5) {
    meusJobs.push(new Cron.Job(i + " * * * *", function(){alert('cron job ' + i + ' just ran')}));
}
    
12.02.2014 / 03:25
0

follows a basic solution that solves part of the simpler example. They can be used as a basis for questions. At the time of this posting it was not fully tested and only implemented cases where the period is more than hour.

No problem copying the entire code of this question , but at least improve it.

If more people suggest changes without creating your own question then I'll open this question as a wiki , but it would interesting someone posting more significant changes as another question to be able to win the bonus.

/**
 * Agendamento de setIntervalo, estilo _cron simples_. Forneça função, e periodo ideal em que ela deveria ser executada
 * que um delay de setTimeout será executado estrategicamente só a partir de um multiplo do tempo ideal, de modo que
 * otimize o tempo de execucução independente da hora quem que for acionada
 * 
 * @param   {Funcion} cb         Callback. Função que será executada
 * @param   {Ingeger} s          Segundo ideal de início
 * @param   {Ingeger} [m]        Minuto ideal de início. Padrão todos os minutos
 * @param   {Ingeger} [h]        Hora ideal de início. Padrão todas as horas.
 * @param   {Ingeger} [interval] Intervalo em milisegundos para ser executada. Padrão diferença entre horários
 * @returns {Ingeger}            Numero, em milisegundos, que a função será executada. Será 
 *                                zero caso esteja no momento exato
 */
function cron (cb, s, m, h, interval) {
    var start = 0, interval = interval || ((s + ((m || 0) * 60) + ((h || 0) * 3600)) * 1000), d = new Date(), tmp;

    tmp = d.getSeconds() - s;
    start = tmp > 0 ? -(tmp - 60) : -tmp;
    console.log(' > start:segundos acumulados ' + start);
    if (m) {
        tmp = m - (d.getMinutes() % m);
        console.log(' > start:minuto (sem ajuste) ' + tmp);
        if (start && tmp === m) {
         tmp -= 1;
        }
        console.log(' > start:minuto (com ajuste) ' + tmp + 'min, eq a '+  tmp * 60 + 's');
        start += tmp > 0 ? tmp * 60 : 0;
    }
    console.log(' > start:segundo+minuto ' + start + 's');

    // @todo implementar hora

    start *= 1000;

    console.log('inicia em a tarefa em ' + start  / 1000 + 's');
    console.log('intervalo será de ' + interval / 1000 + 's');

    setTimeout(function () {
        setInterval(cb, interval);
    }, start);
    return start;
}

Output example using current function

console.log('Iniciado em ' + (new Date()).toISOString().replace('T', ' ').substr(0, 19));
cron(function () {
    console.log('Executado em ' + (new Date()).toISOString().replace('T', ' ').substr(0, 19));
}, 30, 5, null, 5 * 60 * 1000);

/*
//Saída do console:
Iniciado em 2014-02-11 15:27:36
 > start:segundos acumulados 54
 > start:minuto (sem ajuste) 3
 > start:minuto (com ajuste) 3min, eq a 180s
 > start:segundo+minuto 234s
inicia em a tarefa em 234s
intervalo será de 300s 
234000
Executado em 2014-02-11 15:36:31 // Idealmente deveria ter sido 15:35:30
Executado em 2014-02-11 15:41:31 // Idealmente deveria ter sido 15:40:30
*/
    
11.02.2014 / 16:46