Usually, the machine that executes our code has limited resources. Doing everything at once might not only hurt, but can also hang our process and make it stop responding altogether.

When we want to crawl 100 websites, we should crawl, for example, 5 at once, so that we don’t take up all the available bandwidth. As soon as one website is crawled, the next one is ready to go.

Generally speaking, all “heavy” operations should be laid out in time. They should not be executed all-at-once, for better performance and to save resources.

Implementation

If you are familiar with my previous post about implementing promises, then you are going to notice many similarities.

class Concurrently<T = any> {
  private tasksQueue: (() => Promise<T>)[] = [];
  private tasksActiveCount: number = 0;
  private tasksLimit: number;

  public constructor(tasksLimit: number) {
    if (tasksLimit < 0) {
      throw new Error('Limit cant be lower than 0.');
    }

    this.tasksLimit = tasksLimit;
  }

  private registerTask(handler) {
    this.tasksQueue = [...this.tasksQueue, handler];
    this.executeTasks();
  }

  private executeTasks() {
    while (this.tasksQueue.length && this.tasksActiveCount < this.tasksLimit) {
      const task = this.tasksQueue[0];
      this.tasksQueue = this.tasksQueue.slice(1);
      this.tasksActiveCount += 1;

      task()
        .then((result) => {
          this.tasksActiveCount -= 1;
          this.executeTasks();

          return result;
        })
        .catch((err) => {
          this.tasksActiveCount -= 1;
          this.executeTasks();

          throw err;
        });
    }
  }

  public task(handler: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) =>
      this.registerTask(() =>
        handler()
          .then(resolve)
          .catch(reject),
      ),
    );
  }
}

export default Concurrently;

We register a given task by adding it to our tasksQueue and then we call executeTasks.

Now we execute as many tasks as our limit allows us — one by one. Each time adding 1 to our counter called tasksActiveCount.

When the executed task finishes, we remove 1 from tasksActiveCount and again call executeTasks.

Below we can see an example of how it works.

The limit is set to 3. The first two tasks are taking very long to process. We can see the third “slot” getting opened from time to time, allowing the next task in the queue to be executed.

Always there are three, no more, no less.

1*MxACL9-7TXYJTpUTQdJIHQ
Executing heavy and light tasks with the limit of 3.

You can see the code in the repository.

Thank you very much for reading! Can you think of any other way of achieving the same effect? Share them down below.

If you have any questions or comments feel free to put them in the comment section below or send me a message.

Check out my social media!

Join my newsletter!

Originally published at www.mcieslar.com on August 28, 2018.