Task Runner with Concurrency Limit
Reported
2 times
Last seen
2024-09-05
First seen
2024-09-05
Active in
2024, 2025
Description
Implement a `TaskRunner` class that executes async tasks with a configurable maximum concurrency limit. **API:** ```javascript const runner = new TaskRunner(3); // max 3 concurrent tasks runner.push(async () => { /* task 1 */ }); runner.push(async () => { /* task 2 */ }); runner.push(async () => { /* task 3 */ }); runner.push(async () => { /* task 4 — queued, runs when a slot opens */ }); ``` **Requirements:** - `push(task)` accepts an async function and returns a Promise that resolves/rejects with the task's result - At most `maxConcurrency` tasks run simultaneously - Tasks exceeding the limit are queued and executed FIFO as slots become available - A failing task should not block other tasks from running - The runner should never stall — even if tasks reject **Example:** ```javascript const runner = new TaskRunner(2); const results = []; runner.push(async () => { await delay(100); return 'A'; }).then(r => results.push(r)); runner.push(async () => { await delay(50); return 'B'; }).then(r => results.push(r)); runner.push(async () => { await delay(10); return 'C'; }).then(r => results.push(r)); // After 110ms: results = ['B', 'C', 'A'] // B finishes first (50ms), C starts and finishes quickly (10ms), then A (100ms) ```
Approach Tips
**Core Pattern:** Queue + active counter + drain loop. ```javascript class TaskRunner { constructor(maxConcurrency) { this.max = maxConcurrency; this.running = 0; this.queue = []; } push(task) { return new Promise((resolve, reject) => { this.queue.push({ task, resolve, reject }); this._drain(); }); } _drain() { while (this.running < this.max && this.queue.length > 0) { const { task, resolve, reject } = this.queue.shift(); this.running++; task() .then(resolve) .catch(reject) .finally(() => { this.running--; this._drain(); // process next queued task }); } } } ``` **Why `.finally()` is critical:** If a task rejects and you only have `.then()`, the running count never decrements and the runner stalls. `.finally()` runs on both resolve and reject. **What interviewers look for:** - Clean separation between queuing and execution - Correct handling of both resolved and rejected promises - Understanding that `_drain` is called both on `push()` AND on task completion - No race conditions (JS is single-threaded, so no mutex needed — but explain why) **Follow-up questions:** - Add a `waitForAll()` method that resolves when all queued tasks complete - Add task priority (higher priority tasks jump the queue) - Add cancellation support (cancel a queued task before it starts) - What if you need this in a multi-threaded language? (Now you need locks) **Common mistakes:** - Forgetting to handle rejections → runner freezes - Calling `_drain` only in `push` but not on completion → tasks stay queued forever - Using `await` instead of `.then()` inside `_drain` → serializes execution
Sources
Rippling
HR Tech/SaaS
Typically appears in: Technical Phone Screen
60 min — LeetCode-style problem in CodePair. Meta-like pace expected.