Hone logo
Hone
Problems

Implement a JavaScript Request Queue

Many applications, especially those interacting with external APIs, need to manage multiple asynchronous requests. A common requirement is to limit the number of concurrent requests to avoid overwhelming a server or exceeding resource limits. This challenge asks you to implement a request queue that manages the execution of asynchronous functions, ensuring no more than a specified number run at the same time.

Problem Description

You need to create a JavaScript class named RequestQueue that can manage a pool of asynchronous tasks. The queue should accept functions that return Promises (representing the asynchronous requests). You must implement a mechanism to limit the number of concurrently executing Promises to a configurable concurrencyLimit.

Key Requirements:

  1. Initialization: The RequestQueue constructor should accept a concurrencyLimit number.
  2. Adding Tasks: A addTask(taskFunction) method should be provided. taskFunction will be a function that, when called, returns a Promise.
  3. Concurrency Control: The queue should ensure that no more than concurrencyLimit tasks are running concurrently.
  4. Execution: When a task is added, if the concurrency limit has not been reached, it should be executed immediately. Otherwise, it should be added to a waiting queue.
  5. Queue Management: When a running task completes, if there are tasks waiting in the queue, the next waiting task should be started.
  6. Return Values: The addTask method should return a Promise that resolves with the result of the taskFunction when it completes.
  7. Error Handling: If a taskFunction rejects, the Promise returned by addTask should also reject with the same error. The queue should continue to process other tasks.

Expected Behavior:

The RequestQueue should act as a rate limiter for asynchronous operations. Tasks should be processed in the order they are added, subject to the concurrency limit.

Edge Cases:

  • A concurrencyLimit of 0 or less should be handled gracefully (perhaps by throwing an error or treating it as 1).
  • Adding tasks when the queue is empty and the limit is not reached.
  • Adding tasks when the queue is full (all slots are occupied).
  • Tasks that take varying amounts of time to complete.
  • Tasks that resolve successfully and tasks that reject.

Examples

Example 1:

// Assume a helper function to simulate async work
const asyncTask = (id, duration) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Task ${id} finished.`);
      resolve(`Result of task ${id}`);
    }, duration);
  });
};

const queue = new RequestQueue(2); // Concurrency limit of 2

const task1 = () => asyncTask(1, 1000);
const task2 = () => asyncTask(2, 500);
const task3 = () => asyncTask(3, 1500);
const task4 = () => asyncTask(4, 300);

console.log("Adding tasks...");

queue.addTask(task1).then(result => console.log(result)); // Starts immediately
queue.addTask(task2).then(result => console.log(result)); // Starts immediately
queue.addTask(task3).then(result => console.log(result)); // Waits for task1 or task2 to finish
queue.addTask(task4).then(result => console.log(result)); // Waits for task1 or task2 to finish

// Expected Console Output (order of "Task X finished." might vary slightly due to timing, but the concurrency is respected):
// Adding tasks...
// Task 1 finished.
// Result of task 1
// Task 2 finished.
// Result of task 2
// Task 4 finished. // Task 4 starts after task 2 finishes
// Result of task 4
// Task 3 finished. // Task 3 starts after task 1 finishes
// Result of task 3

Example 2:

const asyncTaskWithError = (id, duration) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 2) {
        console.log(`Task ${id} failing.`);
        reject(`Error from task ${id}`);
      } else {
        console.log(`Task ${id} finished.`);
        resolve(`Result of task ${id}`);
      }
    }, duration);
  });
};

const queue = new RequestQueue(1); // Concurrency limit of 1

const taskA = () => asyncTaskWithError('A', 1000);
const taskB = () => asyncTaskWithError('B', 500); // This task will reject
const taskC = () => asyncTaskWithError('C', 700);

console.log("Adding tasks with potential error...");

queue.addTask(taskA)
  .then(result => console.log("Task A completed:", result))
  .catch(error => console.error("Task A failed:", error)); // Should not happen

queue.addTask(taskB)
  .then(result => console.log("Task B completed:", result))
  .catch(error => console.error("Task B failed:", error)); // Should catch the error

queue.addTask(taskC)
  .then(result => console.log("Task C completed:", result))
  .catch(error => console.error("Task C failed:", error)); // Should not happen

// Expected Console Output:
// Adding tasks with potential error...
// Task A finished.
// Task A completed: Result of task A
// Task B failing.
// Task B failed: Error from task B
// Task C finished. // Task C starts after task B finishes (even though B failed)
// Task C completed: Result of task C

Example 3: Zero Concurrency Limit

const queue = new RequestQueue(0); // Concurrency limit of 0

const taskX = () => new Promise(resolve => setTimeout(() => resolve("Task X done"), 100));

console.log("Adding task with concurrency limit 0...");
queue.addTask(taskX)
  .then(result => console.log(result))
  .catch(error => console.error("Error:", error));

// Expected Console Output:
// Adding task with concurrency limit 0...
// Error: Concurrency limit must be at least 1.

Constraints

  • The concurrencyLimit must be a positive integer. If 0 or a negative number is provided, an error should be thrown during instantiation.
  • taskFunction will always be a function that returns a Promise.
  • The maximum number of concurrent tasks running will not exceed concurrencyLimit.
  • The order of task execution should generally follow the order of addition, respecting the concurrency limit.
  • The system should be able to handle a large number of tasks (e.g., up to 1000 tasks).

Notes

  • Consider how to keep track of the number of currently active tasks.
  • A separate queue or array might be useful for storing tasks that are waiting to be executed.
  • When a task completes (either resolves or rejects), you need to trigger the execution of the next waiting task if available and if the concurrency limit allows.
  • The addTask method itself needs to return a Promise that reflects the outcome of the task it adds.
  • Think about how to handle the state of the queue: idle, running, waiting.
Loading editor...
javascript