Worker¶
A Worker executes tasks delegated by a Manager. Workers are responsible for performing blocking, CPU-intensive, or long-running operations outside of the Manager’s main thread.
A developer must explicitly choose the worker type depending on the workload.
Worker types¶
All Workers inherit from the abstract Worker base class. There are two concrete worker types:
WorkerThreadWorkerProcess
There is no single "default" Worker you should use without thinking. You must decide which execution model fits your task.
WorkerThread¶
Runs in the same process as the Manager.
Shares memory with the Manager.
Subject to Python’s GIL.
Suitable for:
I/O-bound tasks
Short-lived operations
Lightweight background work
Because memory is shared, you technically can share objects — but this should be done carefully.
WorkerThread receives the same event instance that was passed to
workers.start(). EDRI does not copy the event, positional arguments, or keyword
arguments. This is also true when max_workers is reached: the pending queue
keeps a reference to the event rather than taking a snapshot.
Do not assume that mutations performed by a thread worker are isolated from the caller. If isolation is required, create a copy or, preferably, pass a dedicated event containing only the data the worker needs (for example an ORM entity ID rather than an ORM instance).
WorkerProcess¶
Runs in a separate process.
Has isolated memory.
True parallel execution (no GIL limitation).
Suitable for:
CPU-bound tasks
Heavy computation
Long-running processing
Large file generation
Running a WorkerProcess in a separate Python environment¶
workers.start accepts an optional environment argument — a Path to a Python virtual environment. When given, that WorkerProcess is launched under <environment>/bin/python instead of the host interpreter:
from pathlib import Path
self.workers.start(event, PdfRenderWorker, environment=Path("/opt/venvs/pdf"))
How it works and what to keep in mind:
The Manager forces the
spawnstart method for that worker and callsset_executable(...)so the child process boots the chosen interpreter.It applies only to
WorkerProcess. For aWorkerThreadthe argument is ignored — threads share the host interpreter and cannot use a different environment.The target environment must be able to import EDRI and the event/worker classes involved: the spawned process unpickles the event and imports your
WorkerProcesssubclass, so the code has to be importable there.
Use this to isolate a worker's dependencies — for example a task that needs a conflicting library version, or a heavy native dependency you do not want in the main application environment.
Worker lifecycle¶
The lifecycle of a Worker is deterministic:
A Manager calls
worker_id = self.workers.start(event, WorkerClass).A lifecycle record is inserted immediately in
self.workersasPENDING.When capacity is available, the runtime is created (
STARTING) and receives the event asself.event.After bootstrap it emits
WorkerStarted, entersRUNNING, and executessetup()followed bydo().The Worker may:
call
self.event_send(event)emit additional events
When execution finishes,
WorkerQuitreports whether the runtime returned, raised, or cooperatively stopped. The Manager then removes the record.
The controller is a read-only mapping. Use self.workers[worker_id], get(),
items(), and the live pending, running, and stopping views for
introspection. Registry mutation is intentionally not exposed.
Cancellation and timeout policy¶
Cancel one execution with self.workers.cancel(worker_id), or start
cancellation for all active executions with self.workers.cancel_all().
Thread workers must cooperate:
while not self.cancel_requested:
do_one_piece()
self.raise_if_cancelled()
# Interruptible alternative to time.sleep(5)
if self.wait_cancelled(5):
return
WorkerThread cannot be safely force-terminated. cancel() is a request to
stop; a thread that ignores it remains in STOPPING and is logged as stuck.
For a WorkerProcess, EDRI may internally escalate from cooperative
cancellation to terminate() and then kill() after the configured grace
periods.
Use an immutable policy for queue, bootstrap, execution, or total lifetime limits:
from edri.abstract import WorkerPolicy
worker_id = self.workers.start(
event,
ReportWorker,
policy=WorkerPolicy(run_timeout=30, cancel_grace=5, terminate_grace=2),
)
Timeouts use a monotonic clock. run_timeout starts at WorkerStarted, so it
includes both setup() and do(). ManagerBase.start_worker() remains as a
temporary deprecated shim, emits DeprecationWarning, and always returns
None.
Communication model¶
⚠ Important difference from Manager behavior
Inside a Worker, you must explicitly send the response event back using self.event_send(...).
Unlike Manager handlers, where setting event.response fields or status is enough (the event is sent automatically after the handler finishes), a Worker does not automatically forward the modified event.
If you modify event.response inside a Worker and do not call self.event_send(event), the Manager will never receive it.
Workers always communicate explicitly.
Workers communicate only via Events.
Sending events¶
self.event_send(event)
This sends the event back to the Manager, which receives it in resolve_callback_worker — the single entry point for every event emitted by any worker. By default the Manager forwards it to the Router (which then routes it to a @response handler or the waiting client); a Manager may override the callback to modify, aggregate, drop, or re-emit worker output first.
See Receiving events back from a Worker for the full behavior, the worker argument, and override examples.
State and isolation¶
Workers must not mutate Manager state directly.
WorkerProcessis fully isolated (separate memory).WorkerThreadshares the event and argument objects supplied toworkers.start(); shared mutation should be avoided unless explicitly designed.
All coordination should happen through events.
Error handling¶
If an exception occurs inside do():
The exception is logged.
The Worker terminates.
WorkerQuitis still emitted.
Workers should handle expected errors explicitly and emit meaningful response events when necessary.
Avoid swallowing exceptions silently.
Generic typing¶
Workers can declare a type parameter:
class MyWorker(WorkerProcess[MyEvent]):
...
This is type-hinting only.
It is not enforced at runtime.
When NOT to use a Worker¶
Do not create a Worker when:
The task is trivial and fast.
The logic does not block.
The operation can safely run inside the Manager method.
Workers are appropriate when:
The task blocks (I/O, network, filesystem).
The task is CPU-heavy.
The task is long-running.
You need isolation.
Creating unnecessary workers increases complexity and overhead.
Minimal example¶
from edri.abstract.worker import WorkerProcess
class MyWorker(WorkerProcess[MyEvent]):
def do(self) -> None:
# Access original event
data = self.event.data
# Perform work
result = heavy_compute(data)
# Emit result
self.event.response.result = result
self.event_send(self.event)
Workers provide controlled concurrency in EDRI. Choose the correct worker type carefully and communicate exclusively through events.