Master/Worker Pattern with Jest Testing
This challenge focuses on implementing a fundamental concurrency pattern: the master/worker model. You will create a system where a "master" process distributes tasks to multiple "worker" processes and collects their results. This pattern is crucial for efficient parallel processing and can significantly improve performance in CPU-bound or I/O-bound applications.
Problem Description
Your task is to build a simplified master/worker system in TypeScript and then write Jest tests to ensure its correct functionality.
The system should consist of:
- Master: A main process responsible for creating worker processes, distributing tasks to them, and aggregating their results.
- Worker: Child processes that receive tasks, perform a specific operation (e.g., a calculation), and send their results back to the master.
You will need to use Node.js's built-in child_process module to spawn worker processes. Communication between the master and workers will be handled via message passing (e.g., process.send and process.on('message')).
Key Requirements:
- The master should be able to spawn a configurable number of worker processes.
- Tasks should be distributed to workers in a round-robin fashion or by selecting an available worker.
- Workers should be able to receive task data, process it, and return a result.
- The master must collect all results from the workers.
- The system should handle cases where a worker might error.
- Your solution should be written in TypeScript.
Expected Behavior: The master process will take a list of tasks. It will distribute these tasks to available workers. Each worker will process its assigned task and send back a result. The master will wait until all tasks are completed and then return an aggregated list of results.
Edge Cases to Consider:
- What happens if there are more tasks than workers?
- What happens if there are fewer tasks than workers?
- What happens if a worker process crashes or throws an error during task processing?
- What if no tasks are provided?
Examples
Example 1: Simple Task Distribution
// In your main application file (e.g., master.ts)
import { spawn } from 'child_process';
import path from 'path';
interface Task {
id: number;
data: number;
}
interface Result {
taskId: number;
workerId: number;
processedData: number;
error?: string;
}
async function runMaster(tasks: Task[], numWorkers: number): Promise<Result[]> {
// ... implementation details ...
return []; // Placeholder
}
// --- Example Usage ---
const tasks = [
{ id: 1, data: 5 },
{ id: 2, data: 10 },
{ id: 3, data: 3 },
{ id: 4, data: 7 },
];
const numberOfWorkers = 2;
// Assuming runMaster is implemented correctly, the expected output would be:
// Example Output (order of results may vary):
// [
// { taskId: 1, workerId: 0, processedData: 10 }, // Worker 0 processed task 1 (5 * 2)
// { taskId: 2, workerId: 1, processedData: 20 }, // Worker 1 processed task 2 (10 * 2)
// { taskId: 3, workerId: 0, processedData: 6 }, // Worker 0 processed task 3 (3 * 2)
// { taskId: 4, workerId: 1, processedData: 14 } // Worker 1 processed task 4 (7 * 2)
// ]
Example 2: Handling More Tasks than Workers
// ... (same interfaces as Example 1) ...
// --- Example Usage ---
const tasks = [
{ id: 1, data: 2 },
{ id: 2, data: 4 },
{ id: 3, data: 6 },
{ id: 4, data: 8 },
{ id: 5, data: 10 },
];
const numberOfWorkers = 2;
// Expected Output (order of results may vary):
// [
// { taskId: 1, workerId: 0, processedData: 4 },
// { taskId: 2, workerId: 1, processedData: 8 },
// { taskId: 3, workerId: 0, processedData: 12 },
// { taskId: 4, workerId: 1, processedData: 16 },
// { taskId: 5, workerId: 0, processedData: 20 }
// ]
Example 3: Worker Error Handling
Suppose a worker encounters an error when processing a task.
// --- Example Usage ---
const tasks = [
{ id: 1, data: 10 },
{ id: 2, data: 'invalid_data' }, // This task will cause a worker to error
{ id: 3, data: 20 },
];
const numberOfWorkers = 2;
// Expected Output (order of results may vary, and one result will indicate an error):
// [
// { taskId: 1, workerId: 0, processedData: 20 },
// { taskId: 2, workerId: 1, error: "Invalid input data for processing" }, // Or a similar error message
// { taskId: 3, workerId: 0, processedData: 40 }
// ]
Constraints
- The master and worker processes should be managed using Node.js
child_process.forkfor inter-process communication. - Task processing in the worker should be a simple operation (e.g., multiplying the input
databy 2). For error handling, simulate an error for a specific input. - Your Jest tests should cover the cases described in the examples, including successful completion, handling a variable number of tasks, and worker errors.
- The maximum number of worker processes to spawn should not exceed 16.
- The number of tasks processed should not exceed 1000 for performance testing.
Notes
- You will need to create at least two files: one for the master logic and one for the worker logic.
- The worker script will need to listen for messages from the parent process and send messages back.
- Consider using Promises and
async/awaitto manage the asynchronous nature of inter-process communication. - For testing, you'll likely want to create a mock or simplified version of your worker script for unit tests, and then use actual
forkcalls in integration tests. - Think about how to gracefully shut down worker processes when all tasks are done.