Javascript/Nodejs setInterval with programmed start, simple cron style

Asked

Viewed 2,718 times

4

For Javascript and Nodejs, there are n robust Cron style libraries, such as Node-cron. But they are complex too much for simple situations, are heavy to download in the browser or require an additional dependency on Nodejs, which makes them unviable in simpler cases.

I want to create a function that can:

  • Accept the second, minute and time the routine is ready to provide new data.
  • Check what time it is now on the client, and schedule the start of the setInterval for the first opportunity when the server has new data.
  • Define the range of setInterval as exactly the period between server updates.
  • Run in Nodejs environment and modern browsers and on IE8. In case you don’t know how to test in Nodejs, I test for you.
  • There should be no additional dependency. No jQuery or Nodejs package.
  • The code shall accept a range parameter of type try again in x seconds, and pass a callback to executed function so that if it returns exactly false, will try this one again until you return true or arrive at the time of the next standard execution. Consider that the server may fail and always return error, but the client should avoid overwriting additional attempts!

Actual use example

The code below is responsible for synchronizing a table of a database with the executed browser or task Nodejs

/**
 * 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 hourly.

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

The problem when using setInterval is that put to update every 15min, runs the risk that when the browser or the Nodejs task is initialized, there is a delay between the time when the client could get new information and the time when it is available. Define setInterval in a period less than 15min would cause data loss.

Bootstrap

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

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);
}
  • 1

    You want a function that does setInterval(func, 5*um_minuto), but synchronized with the clock? 12:30, then 12:45, that’s it?

  • Think of the example: a football site announces 15 minutes of active games metrics, but this site falls and your Crawler restarts frequently. How to make a Crawler that can start at any time prefer to access the page at times when it has updated data, without forcing a setTimeout every minute and running the risk of being banned? There are n practical examples of this, where a setInterval that takes into account the time is time is better than desperately getting data every instant.

  • If the update is done every 15min from 00h00min of the day, and by setTimeout with 15min of dalay, but the routine start at 12h14min, you would always run the function 14min after the information is available.

  • As soon as the user goes online, the time of the server is passed to him, time that is to run the first update and then time of repetition of the process. Already solved. If the guy is offline he will not download anything at all...

  • Gabriel, for the sake of simplification, you don’t have to worry about the server passing the initial time. Simplify and think more about time being synchronized between the local machine and the server that is accessed. Javascript does not need to solve all problems

  • 1

    @Emersonrochaluiz And why can’t you just discount the refresh interval from past current time to the first scheduling? In your example, 15 - 14 = 1, the first execution would be scheduled in 1 minute, and the rest interval would be maintained in 15 minutes.

  • @Luizvieira that’s exactly what. I’m rewarding 50 points whoever does at least that. If you have more answers, the differential would be if the callback of the scheduled function returns false, it tries a few moments later. For simplification, assume that the new Date() of javascript be synchronized to the desired time.

  • One thing I still don’t understand in your question: do you want to minimize the check interval of the current time, or run as close as possible to the server updates? The setInterval tends to go lagging, so with time the trend is to update each time later in relation to the server, unless you check the timestamp constantly. Or maybe you’re looking for a compromise?

  • Only later I realized that considering calculating the delay between steps is not a good idea and that it is better to give by default, so it tends to be interesting beyond the typical start time, optionally accept the explicit interval, like the basis of the answer I made there that needed to be improved. I believe that it would be best to only check current time when starting the function, it defines how much time to get to the first stage (here it is assumed that new Date() will be synchronized) and then passes a value to setTimeout what would you call setInterval, that would then call the function.

Show 4 more comments

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);
    }
}

Use:

// 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 cancel before starting, and the callback receives the value of the new setTimeout when there is repetition, so you can cancel in 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);
  • What a great answer! Up to the moment you were the closest to the result. The only reason I’m not giving the bonus now is because I have to test the case with an initial interval and repetition time. But looking at the code, I can already say that your approach was very interesting and minimalist

  • It is, but I think it is not correct, I correct tomorrow (time of Portugal here).

  • No problem. I saw that this is just the part that is not okay, but the way you approach it is even more flexible than I imagined. Little and your answer will be right.

  • I’ve already updated, now run as expected, and also returns the first timeout to cancel before starting, passes the next timeout to callback to cancel in the middle, and receives a threshold not to run already if there is little time left for the next iteration.

  • His answer is a work of art, @acelent. In 25 lines of code he did something even more flexible than I imagined. I couldn’t test all the possibilities of your code, and I’ll come back here if I find any problems, but from now on, congratulations. The way you approached the problem was genius!

3

A simple cron script, I just made some fixes in 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 of use:

// 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, even because if you will not synchronize with server, will probably have difference of values, but it would be simple to add. runs every minute to check for any script in the cron list to run.

It is easy to add and remove routines to be executed from the list of processes. 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')}));
}
  • Question: how to perform a task each without having to add 30 more lines just to start it? There is no way without rewriting it completely. Another point is that checking to implement seconds although it is trivial, can toast processing unnecessarily and in the case of a node at NodeJS who is busy at a specific time, could imply in the time that running is not the exact time and he ignore the execution completely. That failure would be worse than using the setInterval classic without programmed delay.

  • But this is still an answer that deserved my +1. It’s just not The answer as it stands for the problem presented. Something it does not do and cron of *Nix does is allow more than one exact time, for example "0.5,10,15,20,25,30,45,50,55 * * *" in classic cron would every 5 minutes. This would avoid extreme case of 30 instruction lines just to do every 2 minutes.

  • @Emersonrochaluiz when setTimeout cannot be executed, it is not ignored, but rather runs as soon as processing is available, a setInterval when it cannot be executed, it is waiting tb, but as it is an interval, if the processing takes 3 times the interval, at the time that release it will perform 3 times in a row the function.

  • @Emersonrochaluiz in cron of linux vc can add */5 to run every 5 minutes. And in this code you can use a for to do the same with 3 lines. I added an example for you :)

  • Well, in that case the treatment of */5 or */2 with the for should be within the routine, not outside it. Also why the question said cron style, and not exactly one cron, as other codes that already exist as the node-cron

  • Simple algorithm, if you want something complex and complete, then it is better to use another one. if filling of Features will become complex. Nothing prevents you from adding different parsers there until you support all desired features.

Show 1 more comment

0

follows a base solution that solves part of the simplest example. You can take as a basis to answer your questions. At the time of this posting it has not been fully tested and only has not implemented cases where the period is past the hour.

It’s okay to copy the entire code from that question, but at least improve it.

If more people suggest changes without creating the question itself I will open this question as wiki, but it would be interesting someone post 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 the 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
*/

Browser other questions tagged

You are not signed in. Login or sign up in order to post.