Search code examples
javascriptecmascript-6async-awaites6-promise

Simple task runner in javascript with waiting


I want to implement something like a task runner which will be pushed new tasks. Each of those tasks could be some async operation like waiting on user or making API calls or something else. The task runner makes sure that at a time only allowed number of tasks can execute, while other tasks will keep on waiting till their turn comes.

class Runner {
  constructor(concurrent) {
    this.taskQueue = []; //this should have "concurrent" number of tasks running at any given time

  }

  push(task) {
    /* pushes to the queue and then runs the whole queue */
  }
}

The calling pattern would be

let runner = new Runner(3);
runner.push(task1);
runner.push(task2);
runner.push(task3);
runner.push(task4);

where task is a function reference which will run a callback at the end by which we may know that it is finished. So it should be like

let task = function(callback) {
  /* does something which is waiting on IO or network or something else*/
  callback(); 
}

So I am pushing a closure to runner like

runner.push(function(){return task(callback);});

I think I might need to add a waitList queue as well. But the tasks are not promise itself, so I don't know how to check if those are finished.

Anyways, I need the right approach.


Solution

  • So I am pushing a closure to runner like

    runner.push(function(){return task(callback);});
    

    looks like missing pieces of the runner are being added to the calling syntax. A more complete runner might look like:

    class Runner {
      constructor(concurrent) {
        this.taskQueue = []; // run at most "concurrent" number of tasks at once
        this.runCount = 0;
        this.maxCount = concurrent;
        this.notifyEnd = this.notifyEnd.bind(this);
      }
      
      notifyEnd() {
        --this.runCount;
        this.run();
      }
    
      run() {
        while( (this.runCount < this.maxCount) && taskQueue.length) {
          ++this.runCount;
          // call task with callback bound to this instance (in the constructor)
          taskQueue.shift()(this.notifyEnd);
        } 
      }
    
      push(task) {
        this.taskQueue.push(task);
        this.run();
      }
    }
    

    Now the runner's push method is called with a function taking a callback parameter. Runner state is contained in the value of runCount, 0 for idle or positive integer for tasks running.

    There remain a couple of issues:

    1. The task may be called synchronously to code adding it to the runner. It lacks the strict approach of Promises that always call a then callback asynchronously from the event queue.

    2. The task code must return normally without error. This is not unheard of in JavaScript, where the host tracker for uncaught promise rejection errors must do the same thing, but it is fairly unusual in application script. The runner's call to the task could be placed in a try/catch block to catch synchronous errors but it should also add code to ignore the error if a callback was received before the task threw a synchronous error - otherwise the running task count could go wrong.

    3. If the task calls the callback multiple times, the running task count will be upset in the runner above.

    Considerations similar to these were behind the development and standardization of the Promise interface. I suggest that after taking into consideration potential drawbacks, if a simple task runner meets all requirements then use one. If additional robustness is required, then promisifying tasks and writing a more promise-centric runner could prove a better alternative.