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:
- Initialization: The
RequestQueueconstructor should accept aconcurrencyLimitnumber. - Adding Tasks: A
addTask(taskFunction)method should be provided.taskFunctionwill be a function that, when called, returns a Promise. - Concurrency Control: The queue should ensure that no more than
concurrencyLimittasks are running concurrently. - 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.
- Queue Management: When a running task completes, if there are tasks waiting in the queue, the next waiting task should be started.
- Return Values: The
addTaskmethod should return a Promise that resolves with the result of thetaskFunctionwhen it completes. - Error Handling: If a
taskFunctionrejects, the Promise returned byaddTaskshould 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
concurrencyLimitof 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
concurrencyLimitmust be a positive integer. If 0 or a negative number is provided, an error should be thrown during instantiation. taskFunctionwill 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
addTaskmethod 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.