Files
JSQuestions/questions/async-scheduler.md
T
2026-02-06 13:52:15 +05:30

3.1 KiB

Async Task Scheduler (Concurrency Limiter)

Summary

Implement a TaskScheduler class that allows adding asynchronous tasks. The scheduler should execute tasks but ensure that no more than N tasks run continuously at the same time. As soon as one finishes, the next pending task should start.

Requirements

  • new TaskScheduler(concurrency) - The class TaskScheduler should accept concurrency (integer) in the constructor. This is the maximum number of tasks which be executed simultaneously.
  • TaskScheduler.addTask(taskFn) : Promise - Adds taskFn to task queue. Returns a Promise that resolves/rejects when the actual task finishes. taskFn is a function that returns a Promise.
  • TaskScheduler.play() - Puts the execution in 'play' mode.
  • TaskScheduler.pause() - Puts the execution in 'paused' mode.
  • In 'play' mode, new tasks from the queue will get picked up and processed.
  • In 'paused' mode, no new tasks will get processed. Already running tasks will run till finish.
  • By default, new TaskScheduler instances should be in 'paused' mode.
  • Tasks should be executed in First-In-First-Out (FIFO) order.
  • (In play mode) If the number of running tasks is less than concurrency, start new task immediately.
  • (In play mode) If the concurrency limit is reached, queue the task.
  • When a running task completes (resolves or rejects), automatically pick the next task from the queue.

Example

class TaskScheduler {
  constructor(concurrency) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  addTask(taskFn) {
    return new Promise((resolve, reject) => {
      const taskWrapper = async () => {
        this.running++;
        try {
          const result = await taskFn();
          resolve(result);
        } catch (err) {
          reject(err);
        } finally {
          this.running--;
          this.runNext();
        }
      };

      if (this.running < this.concurrency) {
        taskWrapper();
      } else {
        this.queue.push(taskWrapper);
      }
    });
  }

  runNext() {
    if (this.queue.length > 0 && this.running < this.concurrency) {
      const nextTask = this.queue.shift();
      nextTask();
    }
  }
}

// Usage
const scheduler = new TaskScheduler(2);
const delay = (ms, val) => new Promise(r => setTimeout(() => r(val), ms));

scheduler.addTask(() => delay(1000, 'A')).then(console.log);
scheduler.addTask(() => delay(500, 'B')).then(console.log);
scheduler.addTask(() => delay(300, 'C')).then(console.log);
// Output: B (at 500ms), A (at 1000ms), C (at 1300ms) - C waits for B to finish.

Follow-ups

  • Priority Queue: Modify addTask to accept a priority level (High, Low). Each priority level gets its own queue. Tasks from the High-priority queue should be picked up first. Only when it is empty, Low-priority tasks will get picked up.
  • Cancellation: Return a cancel function from addTask (alongwith the promise). If cancelled while in the queue, it should never run.
  • Error Handling: What happens if taskFn throws synchronously instead of returning a Promise? Ensure the scheduler doesn't crash.