JavaScript Async Pool for Concurrent Task Execution
In modern JavaScript applications, especially those involving I/O operations or external API calls, managing concurrency is crucial for performance and responsiveness. This challenge asks you to build a robust asynchronous pool that limits the number of concurrently running asynchronous tasks, preventing resource exhaustion and ensuring smooth operation.
Problem Description
You need to create a JavaScript class, AsyncPool, that manages a pool of asynchronous tasks. The primary goal of this pool is to limit the number of concurrently executing asynchronous operations. When tasks are added to the pool, they should be executed only when there is an available slot within the concurrency limit. If all slots are occupied, new tasks should wait until a running task completes.
Key Requirements:
- Concurrency Limit: The
AsyncPoolmust accept aconcurrencylimit during instantiation. This number dictates how many asynchronous tasks can run simultaneously. - Task Submission: A method, let's call it
run, should be available to submit asynchronous functions (promises or async functions) to the pool. This method should return a Promise that resolves with the result of the submitted task. - Queueing Mechanism: Tasks that cannot be started immediately due to the concurrency limit should be placed in a queue and executed in the order they were submitted (First-In, First-Out).
- Error Handling: If an asynchronous task throws an error, the Promise returned by
pool.run()for that specific task should reject with the thrown error. The pool should continue to operate, allowing other tasks to run. - Pool Completion: A method, let's call it
drain, should be available. This method should return a Promise that resolves when all currently queued and running tasks have completed.
Expected Behavior:
When pool.run(asyncTask) is called:
- If the number of currently running tasks is less than the
concurrencylimit,asyncTaskshould be executed immediately. - If the number of currently running tasks is equal to or greater than the
concurrencylimit,asyncTaskshould be added to a waiting queue. - When a running task finishes, if there are tasks in the waiting queue, the next task from the queue should be started, provided the concurrency limit is not exceeded.
Examples
Example 1:
// Assume AsyncPool class is defined elsewhere
const pool = new AsyncPool(2); // Concurrency limit of 2
const task1 = () => new Promise(resolve => setTimeout(() => {
console.log("Task 1 finished");
resolve("Result 1");
}, 1000));
const task2 = () => new Promise(resolve => setTimeout(() => {
console.log("Task 2 finished");
resolve("Result 2");
}, 500));
const task3 = () => new Promise(resolve => setTimeout(() => {
console.log("Task 3 finished");
resolve("Result 3");
}, 700));
(async () => {
console.log("Submitting tasks...");
const results = await Promise.all([
pool.run(task1),
pool.run(task2),
pool.run(task3)
]);
console.log("All tasks submitted. Results:", results);
await pool.drain();
console.log("Pool drained.");
})();
Expected Output (timing may vary slightly due to event loop scheduling):
Submitting tasks...
Task 1 finished
Task 2 finished
Task 3 finished
All tasks submitted. Results: [ 'Result 1', 'Result 2', 'Result 3' ]
Pool drained.
Explanation:
task1 and task2 start immediately as the concurrency limit is 2. task3 waits because both slots are occupied. Once task2 finishes (after 500ms), task3 starts. task1 finishes after 1000ms. drain() waits for all three to complete.
Example 2:
// Assume AsyncPool class is defined elsewhere
const pool = new AsyncPool(1); // Concurrency limit of 1
const taskWithError = () => new Promise((resolve, reject) => setTimeout(() => {
console.log("Task with error about to throw");
reject(new Error("Something went wrong"));
}, 300));
const taskAfterError = () => new Promise(resolve => setTimeout(() => {
console.log("Task after error finished");
resolve("Result after error");
}, 200));
(async () => {
console.log("Submitting tasks...");
try {
await pool.run(taskWithError);
} catch (error) {
console.error("Caught error:", error.message);
}
const result = await pool.run(taskAfterError);
console.log("Result of task after error:", result);
await pool.drain();
console.log("Pool drained.");
})();
Expected Output:
Submitting tasks...
Task with error about to throw
Caught error: Something went wrong
Task after error finished
Result of after error: Result after error
Pool drained.
Explanation:
With a concurrency of 1, taskWithError runs. It rejects. The catch block handles the error. taskAfterError then runs and completes successfully. drain() waits for both to finish.
Constraints
- The
concurrencylimit will be a positive integer greater than or equal to 1. - Tasks submitted to the pool will be functions that return a
Promise. - The
runmethod should return aPromise. - The
drainmethod should return aPromise. - The pool should handle a large number of tasks (e.g., up to 1000 tasks) without significant performance degradation.
Notes
- Consider how you will keep track of currently running tasks and tasks waiting in the queue.
- Think about how to trigger the execution of the next queued task when a running task completes.
- The
drainmethod should only resolve after all tasks submitted viarunhave either resolved or rejected. - You might find it helpful to use a counter for active tasks and a standard array for the queue.