Running an operation periodically when the length of operation is not known

Viewed 67

I need to execute an operation that needs to be executed relatively fast (let's say 10 times per second. It should be fast enough, but I can sacrifice speed if there are issues.) This is an ajax request, so potentially I do not know how much time it takes - it could even take seconds if network is bad.

The usual:

setInterval(() => operation(), 100);

Will not work here, because if the network is bad and my operation takes more than 100 ms, It might be scheduled one after another, occupying JS engine time (please correct me if I'm wrong)

The other possible solution is to recursively run it:

function execute() {
   operation();
   setTimeout(execute, 100);
}

This means that there will be 100 ms between the calls to operation(), which is OK for me. The problem with this is that I'm afraid that it will fail at some point because of stack overflow. Consider this code:

i = 0;
function test() { if (i % 1000 == 0) console.log(i); i++; test(); }

If I run it my console, this fails in around 12000 calls. if I add setTimeout in the end, this would mean 12000 / 10 / 60 = 20 minutes, potentially ruining the user experience.

Are there any simple ways how to do this and be sure it can run for days?

2 Answers

There's no "recursion" in asynchronous JavaScript. The synchronous code (the test function) fails because each call occupies some space in the call stack, and when it reaches the maximum size, further function calls throw an error.

However, asynchrony goes beyond the stack: when you call setTimeout, for example, it queues its callback in the event loop and returns immediately. Then, the code, that called it can return as well, and so on until the call stack is empty. setTimeout fires only after that.

The code queued by setTimeout then repeats the process, so no calls accumulate in the call stack.

Therefore, "recursive" setTimeout is a good solution to your problem.

Check this example (I recommend you to open it in fullscreen mode or watch it in the browser console):

Synchronous example:

function synchronousRecursion(i){ 
  if(i % 5000 === 0) console.log('synchronous', i)
  synchronousRecursion(i+1);
  //The function cannot continue or return, waiting for the recursive call
  //Further code won't be executed because of the error
  console.log('This will never be evaluated')
}

try{
  synchronousRecursion(1)
}catch(e){
  console.error('Note that the stack accumuates (contains the function many times)', e.stack)
}
/* Just to make console fill the available space */
.as-console-wrapper{max-height: 100% !important;}

Asynchronous example:

function asynchronousRecursion(i){ 
  console.log('asynchronous',i)
  console.log('Note that the stack does not accumuate (always contains a single item)', new Error('Stack trace:').stack)
  setTimeout(asynchronousRecursion, 100, i+1);
  //setTimeout returns immediately, so code may continue
  console.log('This will be evaluated before the timeout fires')
  //<-- asynchronusRecursion can return here
}

asynchronousRecursion(1)
/* Just to make console fill the available space */
.as-console-wrapper{max-height: 100% !important;}

The two alternatives you showed here actually share the flaw you're concerned about, which is that the callbacks might bunch up and run together. Using setTimeout like this is (for your purposes) identical to calling setInterval (except for some small subtleties that don't apply with a light call like making an AJAX request.)

It sounds like you might want to guarantee that the callbacks run in order, or potentially that if multiple callbacks come in at once, that only the most recent one is run.

To build a service that runs the most recent callback, consider a setup like this:

let lastCallbackOriginTime = 0;

setInterval(()=>{
    const now = new Date().getTime();
    fetch(url).then(x=>x.json()).then(res=>{
        if ( now > lastCallbackOriginTime ) {
            // do interesting stuff 
            lastCallbackOriginTime = now;
        }
        else console.log('Already ran a more recent callback');
    });
}, 100);

Or let's make it run the callbacks in order. To do this, just make each callback depend on a promise returned by the previous one.

let promises = [Promise.resolve(true)];
setInterval(()=>{
    const promise = new Promise((resolve, reject)=> {
fetch(url).then(x=>x.json()).then(serviceResponse=>{
        const lastPromise = promises[promises.length - 1];
        lastPromise.then(()=>resolve(serviceResponse));
    }).then((serviceResponse)=>{
        // Your actual callback code 
    });
    promises.push(promise)

   });
}, 100);


Related