MailboxHandler is a mailbox-patternEventHandler abstract base class. It enqueues incoming events into an internal mailbox queue, and subclasses implement the process() method to dequeue and handle events one at a time. Ideal for scenarios requiring serial consumption, backpressure control, or a custom task loop.
Unlike directly subclassing EventHandler and implementing handle(), MailboxHandler decouples "receiving events" and "processing events" into two separate coroutines:
handle() — runs in the bus dispatch context, only responsible for enqueuing events into the mailbox.
process() — runs in its own asyncio.Task, consuming events from the queue at its own pace.
Event patterns to subscribe to (kept as-is; ShutdownEvent is no longer auto-appended).
config
MailboxConfig \| None
Mailbox configuration. Uses defaults when None.
process()
@abstractmethod async
Custom task loop that subclasses must implement. Use await self.get() to fetch the next event.
get()
async → (Event, Proxy)
Dequeues the next (event, bus proxy) from the mailbox. Blocks when the queue is empty.
on_activate(bus)
hook
Starts the process() task when the registry is bound to a bus (activate).
on_deactivate(bus)
hook
Cancels the task and clears the backlog on bus unbind / unregister. Late in-flight put()s after shutdown are dropped (rolled back) rather than left as orphaned backlog.
bus
EventBus \| None
The bound EventBus instance, available after activation (on_activate).
is_running
bool
Whether the process() background task is currently running.
Event arrives
│
▼
handle() → put(event, proxy) → Enqueue
│
▼
process() coroutine (independent Task)
│
▼
await get() → Dequeue (event, proxy)
│
▼
Business logic (publish new events via proxy.publish())
│
▼
Loop back to get()
Hook-driven start: on_activate creates the process() background asyncio.Task when the handler is registered to an active bus, or when the bus starts (registry.activate).
Enqueue: handle() places (Event, EventBus.Proxy) tuples into the internal asyncio.Queue.
Serial consumption: process() loops on await self.get() to handle events one by one.
Exception restart: If process() exits due to an exception other than CancelledError / StopMailbox, _process_loop waits restart_delay + jitter seconds, then re-invokes process().
Voluntary stop: Raising StopMailbox inside process() exits the loop immediately with no restart.
Hook-driven shutdown: On bus stop (registry.deactivate), on_deactivate cancels the process() task and clears the backlog.
frominfinity_busimportEvent,EventBusfrominfinity_bus.templatesimportMailboxHandlerclassMyHandler(MailboxHandler):def__init__(self):super().__init__(subscriptions=['my.event'])self.received:list[Event]=[]asyncdefprocess(self)->None:whileTrue:event,proxy=awaitself.get()self.received.append(event)# Publish new events via proxy.publish()
classRobustHandler(MailboxHandler):def__init__(self):super().__init__(subscriptions=['task.request'],config=MailboxConfig(restart_delay=0.5,restart_jitter=0.2),)asyncdefprocess(self)->None:whileTrue:event,proxy=awaitself.get()try:awaitself._handle_task(event)exceptException:# Failure of a single event continues to the next.# Only a crash of process() as a whole triggers a restart.pass
frominfinity_bus.templatesimportMailboxHandler,StopMailboxclassDrainThenStopHandler(MailboxHandler):"""ends the consumption loop after a stop command"""def__init__(self):super().__init__(subscriptions=['task.command'])asyncdefprocess(self)->None:whileTrue:event,proxy=awaitself.get()ifevent.dataisnotNoneandgetattr(event.data,'action',None)=='stop':raiseStopMailbox# exits the loop, no restartawaitself._handle(event)
classBusAwareHandler(MailboxHandler):def__init__(self):super().__init__(subscriptions=['status.check'])asyncdefprocess(self)->None:whileTrue:event,proxy=awaitself.get()# Access the raw bus via self.busifself.busandself.bus.is_running:awaitproxy.publish('status.reply',{'ok':True})
process() must be an infinite loop: _process_loop re-invokes process() after it returns normally. If process() runs once and returns, it will be called again immediately, creating a busy loop. Always wrap your logic in while True.
Event loss on crash: If process() crashes after get() returns but before processing completes, that event is lost. The restart loop calls get() again for the next event — it does not retry the lost one.
No implicit ShutdownEvent subscription: Shutdown is driven by the on_deactivate hook; if you subscribe to ShutdownEvent yourself it is just enqueued as an ordinary event.
Bus bound on activation: self.bus is set in on_activate (bus activation) and cleared in on_deactivate. The single-bus registry constraint prevents cross-bus confusion.
Restart vs. stop: CancelledError (external cancellation) and StopMailbox (voluntary stop) both exit the loop without restart; any other Exception triggers a restart after the delay. KeyboardInterrupt and SystemExit (subclasses of BaseException) are not caught and will propagate upward.
Mailbox after stop: raising StopMailbox only ends the consumption task (is_running becomes False); events arriving later are still enqueued but never consumed. To stop receiving entirely, unregister the handler or stop the bus.