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

# 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 — 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.

# 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.