23

How could I accurately run a function when the minute changes? Using a setInterval could work if I trigger it right when the minute changes. But I'm worried setInterval could get disrupted by the event-loop in a long-running process and not stay in sync with the clock.

How can I run a function accurately when the minute changes?

fancy
  • 48,619
  • 62
  • 153
  • 231

9 Answers9

35

First off, you should use setInterval for repeating timers, since it (tries to) guarantee periodic execution, i.e. any potential delays will not stack up as they will with repeated setTimeout calls. This will execute your function every minute:

var ONE_MINUTE = 60 * 1000;

function showTime() {
  console.log(new Date());
}

setInterval(showTime, ONE_MINUTE);

Now, what we need to do is to start this at the exact right time:

function repeatEvery(func, interval) {
    // Check current time and calculate the delay until next interval
    var now = new Date(),
        delay = interval - now % interval;

    function start() {
        // Execute function now...
        func();
        // ... and every interval
        setInterval(func, interval);
    }

    // Delay execution until it's an even interval
    setTimeout(start, delay);
}

repeatEvery(showTime, ONE_MINUTE);
Linus Thiel
  • 38,647
  • 9
  • 109
  • 104
  • 7
    Nice answer but from my tests this is inaccurate. Each interval skews anywhere between .007 and .011 seconds. And will continue to do so because the delay is not recalculated. So if you want accuracy on the long term this would not be an ideal solution. A workaround with this solution is to recall `repeatEvery` instead of `setInterval`. This forces it to recalculate the delay. In my tests this gets it accurate within .001 to .002 seconds every interval. And if there is a slight stray it can get fixed on the next interval. – Bacon Brad Jan 12 '18 at 00:15
2

This may be an idea. The maximum deviation should be 1 second. If you want it to be more precise, lower the milliseconds of setTimeout1.

setTimeout(checkMinutes,1000);

function checkMinutes(){
  var now = new Date().getMinutes();
  if (now > checkMinutes.prevTime){
    // do something
    console.log('nextminute arrived');
  }
  checkMinutes.prevTime = now;
  setTimeout(checkChange,1000);
}

1 But, see also this question, about accuracy of timeouts in javascript

Community
  • 1
  • 1
KooiInc
  • 119,216
  • 31
  • 141
  • 177
  • If in his application a callback from a previous function gets triggered just before checkChange that callback might take 1-2 seconds (if he needs to process something on the spot), so he cannot be 100% sure, right? – alessioalex May 29 '12 at 08:17
  • True. That's why I added a footnote. Aside from that: a method taking 1-2 seconds processing time would trigger me to optimize my scripting. – KooiInc May 29 '12 at 08:23
  • I'm not positive, but using recursion like this may result in a stack overflow at some point. If you have a long-running script, this might be something to consider. Here's a related article: http://www.thinkingincrowd.me/2016/06/06/How-to-avoid-Stack-overflow-error-on-recursive/ – counterbeing Feb 19 '18 at 23:51
1

You can try to be as accurate as you can, setting a timeout each X milliseconds and check if the minute has passed and how much time has passed since the last invocation of the function, but that's about it.

You cannot be 100% sure that your function will trigger exactly after 1 minute, because there might be something blocking the event-loop then.

If it's something vital, I suggest using a cronjob or a separate Node.js process specifically for that (so you can make sure the event loop isn't blocked).

Resources:

http://www.sitepoint.com/creating-accurate-timers-in-javascript/

alessioalex
  • 62,577
  • 16
  • 155
  • 122
1

I've put up a possible solution for you:

/* Usage:
 *
 * coolerInterval( func, interval, triggerOnceEvery);
 *
 *   - func : the function to trigger
 *   - interval : interval that will adjust itself overtime checking the clock time
 *   - triggerOnceEvery : trigger your function once after X adjustments (default to 1)
 */
var coolerInterval = function(func, interval, triggerOnceEvery) {

    var startTime = new Date().getTime(),
        nextTick = startTime,
        count = 0;

    triggerOnceEvery = triggerOnceEvery || 1;

    var internalInterval = function() {

        nextTick += interval;
        count++;

        if(count == triggerOnceEvery) {

            func();
            count = 0;
        }

        setTimeout(internalInterval, nextTick - new Date().getTime());
    };

    internalInterval();

};

The following is a sample usage that prints the timestamp once every minute, but the time drift is adjusted every second

coolerInterval(function() {

    console.log( new Date().getTime() );

}, 1000, 60);

It's not perfect, but should be reliable enough. Consider that the user could switch the tab on the browser, or your code could have some other blocking tasks running on the page, so a browser solution will never be perfect, it's up to you (and your requirements) to decide if it's reliable enough or not.

BFil
  • 12,966
  • 3
  • 44
  • 48
1

Tested in browser and node.js

  • sleeps until 2 seconds before minute change then waits for change
  • you can remove logging as it gets pretty cluttered in log otherwise

        function onMinute(cb,init) {
            
            if (typeof cb === 'function') {
            
                var start_time=new Date(),timeslice = start_time.toString(),timeslices = timeslice.split(":"),start_minute=timeslices[1],last_minute=start_minute;
                var seconds = 60 - Number(timeslices[2].substr(0,2));
                var timer_id;
                var spin = function (){
                                    console.log("awake:ready..set..");
                                    var spin_id = setInterval (function () {
                                        
                                        var time=new Date(),timeslice = time.toString(),timeslices = timeslice.split(":"),minute=timeslices[1];
                                        if (last_minute!==minute) {
                                            console.log("go!");
                                            clearInterval(spin_id);
                                            last_minute=minute;
                                            cb(timeslice.split(" ")[4],Number(minute),time,timeslice);   
                                            console.log("snoozing..");
                                            setTimeout(spin,58000);
                                        }
                                
                                    },100);
                                    
                                };
                
                setTimeout(spin,(seconds-2)*1000);
                
                if (init) {
                    cb(timeslice.split(" ")[4],Number(start_minute),start_time,timeslice,seconds);   
                }
            }
        
        }
        
        
        onMinute(function (timestr,minute,time,timetext,seconds) {
            if (seconds!==undefined) {
                console.log("started waiting for minute changes at",timestr,seconds,"seconds till first epoch");
            } else {
                console.log("it's",timestr,"and all is well");
            }
        },true);
jon
  • 11
  • 1
1

This is a fairly straightforward solution ... the interval for the timeout is adjusted each time it's called so it doesn't drift, with a little 50ms safety in case it fires early.

function onTheMinute(callback) {
    const remaining = 60000 - (Date.now() % 60000);
    setTimeout(() => {
        callback.call(null);
        onTheMinute(callback);
    }, remaining + (remaining < 50 ? 60000 : 0));
}
geofh
  • 535
  • 3
  • 15
0

My first thought would be to use the Date object to get the current time. This would allow you to set your set interval on the minute with some simple math. Then since your worried about it getting off, every 5-10 min or whatever you think is appropriate, you could recheck the time using a new date object and readjust your set interval accordingly.

This is just my first thought though in the morning I can put up some code(its like 2am here).

Ryan
  • 5,644
  • 3
  • 38
  • 66
0

Here's yet another solution based on @Linus' post and @Brad's comment. The only difference is it's not working by calling the parent function recursively, but instead is just a combination of setInterval() and setTimeout():

function callEveryInterval(callback, callInterval){
    // Initiate the callback function to be called every
    // *callInterval* milliseconds.
    setInterval(interval => {
        // We don't know when exactly the program is going to starts 
        // running, initialize the setInterval() function and, from 
        // thereon, keep calling the callback function. So there's almost
        // surely going to be an offset between the host's system 
        // clock's minute change and the setInterval()'s ticks.
        // The *delay* variable defines the necessary delay for the
        // actual callback via setTimeout().
        let delay = interval - new Date()%interval
        setTimeout(() => callback(), delay)
    }, callInterval, callInterval)
}

Small, maybe interesting fact: the callback function only begins executing on the minute change after next.

K.Sy
  • 90
  • 1
  • 3
  • 10
0

The solution proposed by @Linus with setInterval is in general correct, but it will work only as long as between two minutes there are exactly 60 seconds. This seemingly obvious assumption breaks down in the presence of a leap second or, probably more frequently, if the code runs on a laptop that get suspended for a number of seconds.

If you need to handle such cases it is best to manually call setTimeout adjusting every time the interval. Something like the following should do the job:

function repeatEvery( func, interval ) {
    function repeater() {
        repeatEvery( func, interval);
        func();
    }
    var now = new Date();
    var delay = interval - now % interval;

    setTimeout(repeater, delay);
}
Marco Righele
  • 2,702
  • 3
  • 23
  • 23