# Offloading CPU-bound work to a WorkerProcess This example shows the canonical pattern for handing a heavy, CPU-bound job off the manager thread: * the manager receives an API request * it spawns a `WorkerProcess` * the worker runs the computation in true parallelism (no GIL) * the worker emits the completed event back * the manager forwards the response to the client The toy task is brute-forcing a numeric password hash; replace `compute(...)` with whatever real CPU-heavy work you have (image processing, compression, simulation, …). --- ## Event and response ```python # events/crack.py from http import HTTPMethod from edri.api.dataclass.api_event import api, ApiType from edri.dataclass.event import Event, event from edri.dataclass.response import Response, response from edri.utility.function import inject from edri.utility.validation import IntegerValidator, StringValidator @response class CrackResponse(Response): found: int | None attempts: int @api(url="/crack", method=HTTPMethod.POST, exclude={ApiType.HTML, ApiType.WS}) class CrackHash(Event): target: inject(StringValidator, minimum_length=1) max_value: inject(IntegerValidator, minimum=1, maximum=10_000_000) = 1_000_000 response: CrackResponse ``` --- ## Worker A `WorkerProcess` runs in its own OS process and shares no memory with the manager. Its `do()` method is the entry point; communication back to the manager goes through `self.event_send(...)`. ```python # workers/crack.py import hashlib from edri.abstract.worker import WorkerProcess from events.crack import CrackHash class CrackWorker(WorkerProcess[CrackHash]): def do(self) -> None: target = self.event.target attempts = 0 found: int | None = None for candidate in range(self.event.max_value): attempts += 1 digest = hashlib.sha256(str(candidate).encode()).hexdigest() if digest == target: found = candidate break self.event.response.found = found self.event.response.attempts = attempts self.event_send(self.event) ``` Important: * Unlike a manager handler, a worker **must** call `event_send(...)` explicitly — modifying `event.response` is not enough. * When `do()` returns (or raises), the framework emits `WorkerQuit` automatically — you do not need to send it yourself. --- ## Manager The manager simply forwards the request to a worker. Because the worker eventually re-emits the event with the response populated, the `resolve_callback_worker` default (forward to router) is fine; the manager doesn't need a custom callback. ```python # managers/crack.py from edri.abstract import ManagerBase, request from events.crack import CrackHash from workers.crack import CrackWorker class CrackManager(ManagerBase): @request def crack(self, event: CrackHash) -> None: # Fire off a process; do NOT fill event.response here. self.workers.start(event, CrackWorker) ``` Why not fill `event.response` and let the manager auto-send? Because that would block the manager thread for the duration of the brute force. Handing the event to a `WorkerProcess` returns control immediately; the response flows back through the worker → manager → router pipeline once `CrackWorker.do()` finishes. --- ## Entry point ```python # main.py from edri import EDRI from managers.crack import CrackManager if __name__ == "__main__": edri = EDRI() edri.add_component(CrackManager(edri.router_queue)) edri.start_api() edri.run() ``` --- ## Calling it ```bash curl -X POST http://localhost:8080/crack \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"target": "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9", "max_value": 100}' ``` (`5feceb...` is the SHA-256 of `"0"`, so the worker reports `found=0` after one attempt.) --- ## When to use WorkerThread instead Choose `WorkerThread` for I/O-bound work (network calls, disk reads, waiting on subprocess output) — threads are lighter than processes and share memory with the manager. For CPU-bound work like this example, the GIL would serialize execution; `WorkerProcess` is the right choice. See {doc}`/worker` for the full comparison.