Hone logo
Hone
Problems

Concurrent Task Scheduler in JavaScript

This challenge asks you to build a simple concurrent task scheduler in JavaScript. A concurrent scheduler allows multiple tasks to run seemingly simultaneously, improving efficiency by utilizing available resources. This is particularly useful in scenarios like web servers handling multiple requests or background processing of data.

Problem Description

You are tasked with creating a ConcurrentScheduler class in JavaScript. This class should manage a queue of tasks and execute them concurrently, up to a specified maximum number of concurrent tasks. Each task is represented by a function that takes no arguments and returns a Promise. The scheduler should ensure that no more than maxConcurrency tasks are running at any given time. Tasks should be executed in the order they are added to the queue. The scheduler should also provide a mechanism to wait for all tasks to complete.

Key Requirements:

  • add(task): Adds a task (a function returning a Promise) to the queue.
  • start(maxConcurrency): Starts the scheduler, allowing up to maxConcurrency tasks to run concurrently. This method should initiate the execution of tasks from the queue.
  • onComplete(callback): Registers a callback function that will be executed when all tasks in the queue have completed.
  • Error Handling: If a task throws an error (rejects its Promise), the error should be caught and logged, but the scheduler should continue processing other tasks. The onComplete callback should still be called when all tasks have finished, regardless of errors.
  • Queue Management: The scheduler should maintain the order of tasks as they are added.

Expected Behavior:

The scheduler should process tasks from the queue in a First-In, First-Out (FIFO) manner. When a task completes (its Promise resolves or rejects), the scheduler should pick the next task from the queue (if available) and start it, respecting the maxConcurrency limit. The onComplete callback should only be invoked after all tasks have finished, regardless of their success or failure.

Edge Cases to Consider:

  • Empty Queue: If the queue is empty when start() is called, the onComplete callback should be invoked immediately.
  • maxConcurrency of 0: If maxConcurrency is 0, no tasks should be executed, and the onComplete callback should be invoked immediately.
  • Tasks Throwing Errors: Tasks might reject their Promises. The scheduler should handle these errors gracefully without crashing and should still call onComplete when all tasks are finished.
  • Multiple Calls to add() after start(): The scheduler should continue to process tasks added after start() has been called.

Examples

Example 1:

Input:
const scheduler = new ConcurrentScheduler();
const task1 = () => new Promise(resolve => setTimeout(() => resolve('Task 1 completed'), 100));
const task2 = () => new Promise(resolve => setTimeout(() => resolve('Task 2 completed'), 50));
const task3 = () => new Promise(resolve => setTimeout(() => resolve('Task 3 completed'), 25));

scheduler.add(task1);
scheduler.add(task2);
scheduler.add(task3);
scheduler.start(2);
scheduler.onComplete(() => console.log('All tasks completed'));

Output: (Order of 'Task 1 completed', 'Task 2 completed', and 'Task 3 completed' may vary, but 'All tasks completed' will be logged last)

Task 1 completed
Task 2 completed
Task 3 completed
All tasks completed

Explanation: Tasks 1 and 2 start concurrently. Task 2 completes first, then Task 3 starts. Finally, Task 1 completes, and the onComplete callback is invoked.

Example 2:

Input:
const scheduler = new ConcurrentScheduler();
const task1 = () => new Promise(resolve => setTimeout(() => resolve('Task 1 completed'), 100));
const task2 = () => new Promise((resolve, reject) => setTimeout(() => reject('Task 2 failed'), 50));
const task3 = () => new Promise(resolve => setTimeout(() => resolve('Task 3 completed'), 25));

scheduler.add(task1);
scheduler.add(task2);
scheduler.add(task3);
scheduler.start(1);
scheduler.onComplete(() => console.log('All tasks completed'));

Output: (Order of 'Task 1 completed' and 'Task 3 completed' may vary, but 'All tasks completed' will be logged last)

Task 1 completed
Task 3 completed
All tasks completed

Explanation: Task 1 starts. Task 2 rejects after 50ms, but the scheduler continues. Task 3 starts after Task 1 completes. The onComplete callback is invoked after both Task 1 and Task 3 have finished.

Example 3:

Input:
const scheduler = new ConcurrentScheduler();
scheduler.start(3);
scheduler.onComplete(() => console.log('All tasks completed'));

Output:

All tasks completed

Explanation: The queue is empty, so onComplete is called immediately.

Constraints

  • maxConcurrency must be a positive integer.
  • Tasks are functions that return Promises.
  • The scheduler should handle errors thrown by tasks gracefully.
  • The onComplete callback should be called exactly once.
  • The scheduler should maintain FIFO order of tasks.

Notes

  • Consider using async/await for cleaner asynchronous code.
  • Think about how to manage the queue of tasks and the currently running tasks.
  • Pay close attention to the onComplete callback and ensure it's called only after all tasks are finished.
  • Error handling is crucial for a robust scheduler. Don't let a single failing task prevent the scheduler from completing its work.
  • The start() method should not return a promise. It should initiate the scheduling process.
Loading editor...
javascript