warren.workers
Workers: the worker base classes, the processing message and the worker runner.
To choose a base class, decide how the worker selects its messages and how
it runs. CapabilityWorkerBase selects by a declared set of accepts
data types. FilteringWorkerBase selects with a hand-written
should_process. Both are async. For a worker without selection, use
AsyncProcessingWorkerBase for async code, or SyncProcessingWorkerBase
for sync code that runs in a thread.
Worker base classes
CapabilityWorkerBase
classCapabilityWorkerBase(worker_name, *, accepts, produces=None, worker_type=None)Async worker that declares its capabilities (accepts / produces).
The declarative counterpart to FilteringWorkerBase: instead of
hand-writing should_process, the worker declares the input
data_types it accepts, and should_process defaults to
"is the message's data_type one I accept?". This doubles as a runtime
type guard (defense in depth) even when a topic/direct exchange has already
routed only matching messages.
accepts / produces are typically supplied from the WorkerSpec
via WorkerFactoryContext (single source of truth, also used by
routing-plan validation).
paramworker_namestrUnique instance identifier. See AsyncProcessingWorkerBase.
paramacceptsfrozenset[str] | set[str] | list[str]Input data_types this worker handles.
paramproducesstr | None= NoneOutput data_type (informational; None if none).
paramworker_typestr | None= NoneDefaults to the class name in snake_case. See AsyncProcessingWorkerBase.
methodshould_process(message) -> boolDefault guard: accept the message iff its data_type is in accepts.
Override for finer-grained selection (e.g. on preprocessing_required).
parammessagedictmethodprocessasync (message) -> dict | NoneProcess a message that passed should_process().
parammessagedictRaises:
SoftFailureException: When failure is retryable.HardFailureException: When failure is permanent.
method__call__async (message) -> dict | NoneparammessagedictFilteringWorkerBase
classFilteringWorkerBase(worker_name, *, worker_type=None)Async worker that self-selects messages via should_process().
Subclasses implement should_process() to decide whether a message is relevant, and process() for the actual work. When should_process() returns False, the message is ACKed with no downstream publish.
paramworker_namestrUnique instance identifier. See AsyncProcessingWorkerBase.
paramworker_typestr | None= NoneDefaults to the class name in snake_case. See AsyncProcessingWorkerBase.
methodshould_process(message) -> boolDecide whether this worker should handle the message.
parammessagedictMessage body as a dictionary.
Returns
boolTrue if this worker should process the message.
methodprocessasync (message) -> dict | NoneProcess a message that passed should_process().
parammessagedictMessage body as a dictionary.
Returns
dict | NoneOptional result to publish downstream, or None if no downstream message is needed (terminal worker).
Raises:
SoftFailureException: When failure is retryable.HardFailureException: When failure is permanent.
method__call__async (message) -> dict | NoneparammessagedictAsyncProcessingWorkerBase
classAsyncProcessingWorkerBase(worker_name, *, worker_type=None)Abstract base class for async processing workers.
Satisfies the AsyncMessageConsumerInterface protocol. Workers
receive plain dictionaries and return optional dictionaries — they
never interact with RabbitMQ or any other message transport directly.
Heartbeat safety: this worker runs on the main event loop alongside
the RabbitMQ connection. Every await yields control so heartbeats
can be processed.
- Pure async I/O (HTTP clients, DB queries) is safe.
- CPU-bound work MUST be offloaded via
asyncio.get_running_loop().run_in_executor(). - If your worker is primarily CPU-bound or uses sync libraries,
use
SyncProcessingWorkerBaseinstead — the framework handles threading automatically.
Use this base class when:
- Worker needs async I/O with shared connection pools (async HTTP clients, async DB clients).
- Worker combines async I/O with some CPU work (offload CPU parts
via
run_in_executor).
paramworker_namestrUnique instance identifier. It is the name of the worker and origin.name on its failure envelopes.
paramworker_typestr | None= NoneDefaults to the class name in snake_case. It is origin.type on failure envelopes. Set it as origin.type on results too: RoutingPlanRouter reads it as the plan node id. Pass WorkerFactoryContext.worker_type.
attributenamestrUnique instance identifier for this worker.
attributetypestrIdentifies the kind of worker (e.g., "parser_worker").
methodsetupasync () -> NoneStart any owned async resources.
Called by the runner after the worker is constructed and before
the consumer begins delivering messages. Override to start
long-lived async resources (subprocesses, HTTP client
pools, connection warm-ups) that cannot be safely initialised
in __init__ because they need the running event loop.
Default: no-op.
methodteardownasync () -> NoneClose any owned async resources.
Called by the runner after the consumer has stopped delivering
messages and before infrastructure (MongoDB, Redis, RMQ) is torn
down. Override to close resources that require an explicit async
moment — async HTTP client pools (cannot close from __del__)
and subprocesses (GC does not reap them).
Default: no-op. Implementations should be best-effort: log and continue rather than raise, so one resource's failure does not strand another.
method__call__async (message) -> dict | NoneProcess a message from the queue.
parammessagedictMessage body as a dictionary.
Returns
dict | NoneOptional result to publish downstream, or None if no downstream message is needed.
Raises:
SoftFailureException: When failure is retryable. Message is retried with back-off (seeSoftFailureException).HardFailureException: When failure is permanent. Message will be dropped. Any other exception counts as a hard failure (seeHardFailureException).
SyncProcessingWorkerBase
classSyncProcessingWorkerBase(worker_name, *, worker_type=None)Abstract base class for sync processing workers.
Satisfies the SyncMessageConsumerInterface protocol. The framework
automatically runs __call__ in a ThreadPoolExecutor via
run_in_executor(), keeping the main event loop free for
RabbitMQ heartbeats.
Threading behavior: each invocation runs in a thread from the default
ThreadPoolExecutor (sized min(32, os.cpu_count() + 4)).
The worker may spawn its own ProcessPoolExecutor or
multiprocessing.Pool internally for CPU parallelism across
cores.
- CPU-bound work and sync library calls (
requests, file I/O, sync DB drivers) work naturally. - Do NOT use
asyncio.run()orasyncio.get_event_loop()— async objects from the main loop cannot be used from a different thread due to event loop affinity. - If you need async I/O (async HTTP clients, async DB clients), use
AsyncProcessingWorkerBaseinstead.
Use this base class when:
- Worker does CPU-bound work (parsing, extraction, computation).
- Worker uses sync libraries (
requests, file I/O, sync DB drivers). - Worker manages its own multiprocessing internally.
paramworker_namestrUnique instance identifier. See AsyncProcessingWorkerBase.
paramworker_typestr | None= NoneDefaults to the class name in snake_case. See AsyncProcessingWorkerBase.
attributenamestrUnique instance identifier for this worker.
attributetypestrIdentifies the kind of worker (e.g., "parser_worker").
methodsetupasync () -> NoneStart any owned async resources. Default: no-op.
See AsyncProcessingWorkerBase.setup for semantics. Sync
workers can still own async resources at the process level (the
runner operates on the event loop even when __call__ runs in
a thread).
methodteardownasync () -> NoneClose any owned async resources. Default: no-op.
See AsyncProcessingWorkerBase.teardown for semantics.
method__call__(message) -> dict | NoneProcess a message from the queue.
parammessagedictMessage body as a dictionary.
Returns
dict | NoneOptional result to publish downstream, or None if no downstream message is needed.
Raises:
SoftFailureException: When failure is retryable. Message is retried with back-off (seeSoftFailureException).HardFailureException: When failure is permanent. Message will be dropped. Any other exception counts as a hard failure (seeHardFailureException).
Messages
ProcessingMessage
dataclassProcessingMessage(data_type, data, job_id, origin, job_metadata=None, job_parameters=dict(), preprocessing_required=list(), preprocessing_completed=list(), retry=None, scope=None, lane=None, priority=None)Canonical message format for the distributed processing framework.
Workers use data_type (and optionally job_metadata) to decide
whether to process a message, and job_parameters to decide how.
paramdata_typestrSemantic type of the payload (e.g., "pdf_document", "markdown_document", "text_chunks").
paramdatadict[str, Any]Payload, or a reference to the payload in storage. The framework reads two keys: doc_id (required for job status: a message without it is not counted) and part_idx (default 0). Together with job_id, they identify a message for retries and logs. All other keys are opaque to the framework.
paramjob_idstrIdentifier grouping related messages into a single job.
paramoriginMessageOriginSource that produced this message.
paramjob_metadatadict[str, Any] | None= NoneOptional metadata about the job. Structure is application-defined (e.g., user, reason, priority).
paramjob_parametersdict[str, Any]= dict()Application-defined processing parameters. Workers interpret these to decide how to process (e.g., force flags for reprocessing).
parampreprocessing_requiredlist[str]= list()Preprocessing steps still needed before the main processing can proceed.
parampreprocessing_completedlist[str]= list()Preprocessing steps already done.
paramretryRetryInfo | None= NoneRetry state, populated on soft failure.
paramscopestr | None= NoneOptional storage scope. The consumer manager sets it as the current scope while the handler runs, and copies it to the result.
paramlanestr | None= NoneOptional application-defined routing lane.
parampriorityint | None= NoneOptional AMQP priority, an integer from 0 through 255. Only the RabbitMQ publisher sends it, and the queue needs an x-max-priority argument (RMQConsumerConfig.queue_arguments). The memory and Kafka backends ignore it.
Raises:
ValueError: Ifpriorityis a bool, or an int outside 0 through 255.
methodcreate_from(message) -> ProcessingMessageCreate a ProcessingMessage from a raw message dict.
parammessagedict[str, Any]Raw message dictionary from the transport layer.
Returns
ProcessingMessageTyped ProcessingMessage instance.
Raises:
KeyError: If required fields are missing.
methodderive(*, data_type, data, origin, preprocessing_required=None, preprocessing_completed=None) -> ProcessingMessageDerive a new message carrying forward job metadata from this one.
Copies job_id, job_metadata, job_parameters, scope,
lane, and priority from self. Preprocessing fields are copied
unless explicitly overridden.
The retry field is intentionally not copied — a derived
message starts with a clean retry state.
paramdata_typestrSemantic type of the new payload.
paramdatadict[str, Any]New payload or reference data.
paramoriginMessageOriginOrigin identifying the worker producing this message.
parampreprocessing_requiredlist[str] | None= NoneOverride preprocessing steps still needed. If None, copies from self.
parampreprocessing_completedlist[str] | None= NoneOverride completed preprocessing steps. If None, copies from self.
Returns
ProcessingMessageNew ProcessingMessage with inherited job metadata.
methodto_dict() -> dict[str, Any]Serialize to dict for publishing via transport layer.
Produces a dict compatible with create_from() — optional
fields (job_metadata, retry) are omitted when None.
MessageOrigin
dataclassMessageOrigin(type, name)Identifies the source that produced a message.
paramtypestrCategory of the origin (e.g., "document_parser", "API").
paramnamestrSpecific instance name (e.g., "document_parser-1").
methodto_dict() -> dict[str, str]Serialize to dict for transport layer.
RetryInfo
dataclassRetryInfo(count, after, reason, max=5)Retry state attached to messages on soft failure.
paramcountintCurrent retry attempt number.
paramafterintDelay in seconds before the next retry.
paramreasonstrExplanation for the retry.
parammaxint= 5Maximum retry attempts before hard failure.
methodto_dict() -> dict[str, Any]Serialize to dict for transport layer.
Runner
WorkerRunnerBase
classWorkerRunnerBase(*, name=None, health=None)Base class for running a worker process.
Provides the generic lifecycle: signal handling, consume loop, and
graceful shutdown. Subclasses implement setup() to wire their
specific components (connections, stores, worker, consumer manager).
A subclass uses these protected names:
self._consumer_manager: set it insetup().run()andteardown()use it.self._mark_setup_succeeded(): call it at the end ofsetup(). Without it,run()raises RuntimeError._on_teardown(): override it for subclass-specific cleanup (e.g., closing connections, cancelling timers). It runs after the consumer is stopped but before state is reset.self._exception_wrapping(description): a context manager for a setup phase. It re-raises a failure asWarrenErrorwith the phase and the worker name.
paramnamestr | None= NoneLogger name of the runner.
paramhealthHealthConfig | None= NoneHealth endpoint and watchdog settings. Defaults to HealthConfig(). Usage:: runner = MyRunner(...) await runner.setup() # subclass wires components await runner.run() # consume until shutdown signal await runner.teardown() # cleanup
methodsetupasync () -> NoneInitialize all resources and wire components.
Subclasses must set self._consumer_manager and call
self._mark_setup_succeeded() at the end.
methodrunasync () -> NoneStart consuming and wait for shutdown signal (SIGINT/SIGTERM).
Raises:
RuntimeError: Ifsetup()did not succeed before.WarrenError: If the watchdog declared the consumer lost.
methodteardownasync () -> NoneStop consuming, run subclass cleanup, and reset state.
Idempotent and safe after a partial or failed setup(): tears
down whatever exists, best-effort, regardless of whether
setup() ran to completion. _setup_succeeded gates
run() only — never cleanup.
ConsumerManagerFactory
type aliasConsumerManagerFactory = Callable[[MessageConsumerInterface], ConsumerManagerInterface]Creates a consumer manager for the given consumer.
Signature: (consumer) -> ConsumerManagerInterface
The factory encapsulates transport-specific wiring (exchange, queue, connection) so runners stay transport-agnostic. The consumer carries its own identity (name and type) for use on failure envelopes.
Health
HealthConfig
modelHealthConfigWatchdog and endpoint settings.
attributeenabledbool= TrueServe /ready and /live. On by default; a failed bind is logged and the worker keeps consuming without the endpoint.
attributehoststr= '0.0.0.0'Bind address.
attributeportint= 8080Bind port; 0 picks a free one (tests).
attributecheck_interval_sfloat= 5.0Seconds between watchdog observations.
attributeprobe_timeout_sfloat= 1.0Upper bound on each observation's waits.
attributeconsumer_lost_grace_sfloat= 60.0How long the consumer may be lost, with a live connection, before the process exits so the orchestrator restarts it.
HealthServer
classHealthServer(get_health, *, host, port)Minimal HTTP readiness endpoint.
GET /ready → 200 while the last sample says ready, else 503;
GET /live → 503 if a handler exceeds twice its timeout, else 200.
Anything else → 404. The
body is the sample as JSON.
paramget_healthCallable[[], ConsumerHealth | None]Returns the last observed health, or None before the first observation.
paramhoststrBind address.
paramportintBind port; 0 picks a free one.
attributeportint | NoneThe bound port, or None while not serving.
methodstartasync () -> boolBind and serve. A failed bind is reported, not raised.
Returns
boolTrue if serving.
methodstopasync () -> NoneMessage keys and identity
build_message_key
functionbuild_message_key(job_id, doc_id, part_idx=0) -> strBuild a composite message key from identity fields.
Consistent key format used across ResultsStore, RetryWorker, and
cache layers. Format: job:{job_id}:doc:{doc_id}:part:{part_idx}.
None values are normalized to "-" so keys remain valid and
consistent regardless of whether identifiers are available.
paramjob_idstr | NoneJob identifier, or None.
paramdoc_idstr | NoneDocument identifier, or None.
parampart_idxint= 0Part index within the document.
Returns
strComposite key string.
build_message_key_prefix
functionbuild_message_key_prefix(doc_id, job_id) -> strBuild a key prefix for matching all parts of a document.
Warning: doc_id is the first parameter, but build_message_key
takes job_id first. Pass the arguments by keyword, or the prefix
matches no key.
Use with CacheInterface.get_by_key_prefix() to retrieve all
parts of a document within a job. Includes trailing separator
to avoid matching unrelated keys.
None values are normalized to "-".
paramdoc_idstr | NoneDocument identifier, or None.
paramjob_idstr | NoneJob identifier, or None.
Returns
strKey prefix string ending with :.
extract_message_identity
functionextract_message_identity(message) -> strExtract identity fields from a message dict for logging.
Returns a human-readable string with job_id, doc_id, and part_idx.
Missing fields are shown as "?". Safe to call on any message dict.
parammessagedictRaw message dictionary.
Returns
strIdentity string for log messages, e.g. "job=abc123 doc=doc456 part=0".
ExtractMessageIdentityFunc
type aliasExtractMessageIdentityFunc = Callable[[dict], str]Callable that extracts a human-readable identity string from a message dict.