Building a Async Concurrency-Controlled Task Manager in JavaScript

In modern web applications, managing asynchronous tasks efficiently is crucial, especially when dealing with tasks that consume resources or depend on external APIs. In this article, we’ll explore how to build a Task Manager in JavaScript that enforces a concurrency limit and queues tasks if the limit is reached. This pattern is essential for creating scalable applications that perform smoothly under load.

Need to build a Concurrency-Controlled Task Manager which:

  • Limits the number of tasks running concurrently.
  • Queues additional tasks when the concurrency limit is reached.
  • Executes queued tasks as soon as a running task completes.

This approach is invaluable in scenarios such as:

  • Managing API requests to avoid hitting rate limits.
  • Handling resource-intensive operations without overwhelming the system.
  • Improving overall application performance by controlling the flow of asynchronous tasks.

Key Focus

  1. Concurrency Limit: The maximum number of tasks that can run at the same time.
  2. Queueing: When the concurrency limit is reached, tasks are placed in a queue and executed sequentially as running tasks finish.

Implementation Strategy

We’ll build a TaskManager class that:

  • Accepts tasks (functions that return promises).
  • Runs tasks up to the defined concurrency limit.
  • Queues additional tasks.
  • Automatically starts a queued task once a running task completes.

Below is an example implementation of a concurrency-controlled Task Manager:

class TaskManager {
  constructor(concurrencyLimit) {
    this.concurrencyLimit = concurrencyLimit;
    this.currentRunning = 0;
    this.taskQueue = [];
  }

  /**
   * Adds a task to the Task Manager.
   * @param {Function} taskFn - A function that returns a promise.
   * @returns {Promise} - A promise that resolves or rejects when the task completes.
   */
  addTask(taskFn) {
    return new Promise((resolve, reject) => {
      const task = async () => {
        try {
          this.currentRunning++;
          const result = await taskFn();
          resolve(result);
        } catch (error) {
          reject(error);
        } finally {
          this.currentRunning--;
          // Start next task in queue if available.
          if (this.taskQueue.length > 0) {
            const nextTask = this.taskQueue.shift();
            nextTask();
          }
        }
      };

      if (this.currentRunning < this.concurrencyLimit) {
        task();
      } else {
        this.taskQueue.push(task);
      }
    });
  }
}

// Example usage:

// Simulate an asynchronous task
const simulateTask = (id, duration) => {
  return () =>
    new Promise((resolve) => {
      console.log(`Task ${id} started.`);
      setTimeout(() => {
        console.log(`Task ${id} completed.`);
        resolve(`Result of Task ${id}`);
      }, duration);
    });
};

const manager = new TaskManager(2); // Allow maximum of 2 tasks concurrently

// Adding tasks with different durations
manager.addTask(simulateTask(1, 3000)).then(console.log);
manager.addTask(simulateTask(2, 2000)).then(console.log);
manager.addTask(simulateTask(3, 1000)).then(console.log);
manager.addTask(simulateTask(4, 4000)).then(console.log);

Explanation

  1. Constructor:
    The TaskManager constructor initializes:
    • concurrencyLimit: The maximum number of tasks allowed to run simultaneously.
    • currentRunning: A counter for currently running tasks.
    • taskQueue: An array that stores tasks waiting to be executed.
  2. addTask Method:
    • Accepts a task function (taskFn) that returns a promise.
    • Returns a new promise that resolves or rejects based on the task’s outcome.
    • Checks if the current number of running tasks is less than the concurrency limit.
      • If yes, it runs the task immediately.
      • Otherwise, it queues the task.
    • After a task completes (whether successfully or not), it decrements the running counter and starts the next task from the queue if available.
  3. Simulated Tasks:
    • The simulateTask function simulates asynchronous tasks by returning a promise that resolves after a given duration.
    • Multiple tasks are added to the TaskManager to demonstrate how tasks are managed concurrently and queued when necessary.

This is also one of the good interview questions, Stay tuned to Rowdy Coders for more in-depth interview articles on JavaScript, system design, and interview preparation. Happy coding!