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
WorkerProcessthe 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¶
# 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(...).
# 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 — modifyingevent.responseis not enough.When
do()returns (or raises), the framework emitsWorkerQuitautomatically — 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.
# 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¶
# 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¶
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 Worker for the full comparison.