EventBus¶
Overview¶
EventBus is the central dispatch hub — responsible for task queuing, concurrency control,
error reporting, and lifecycle management. Proxy is the only interface for publishing events.
The system includes two built-in special events: ShutdownEvent and TaskErrorEvent.
EventBus¶
class EventBus:
def __init__(
self,
event_registry: EventRegistry,
handler_registry: EventHandlerRegistry,
queue: Optional[EventQueue] = None,
router: Optional[Router] = None,
max_handler_semaphore: int = 256,
shutdown: ShutdownConfig = ShutdownConfig(),
middleware_chain: Optional[MiddlewareChain] = None
) -> None
# Lifecycle
async def start(self) -> None
async def stop(self) -> None
async def __aenter__(self) -> "EventBus"
async def __aexit__(self, ...) -> Optional[bool]
# Proxy
def proxy(self, source: str, raw_event: Optional[Event] = None) -> Proxy
# Router
@property
def router(self) -> Router
# Observability
@property
def is_running(self) -> bool
@property
def is_publishing_enabled(self) -> bool
@property
def active_task_count(self) -> int
@property
def queue_size(self) -> int
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
event_registry |
EventRegistry |
(required) | Event registry instance. |
handler_registry |
EventHandlerRegistry |
(required) | Handler registry instance. |
queue |
Optional[EventQueue] |
None |
Internal dispatch queue; the bus depends only on the EventQueue abstraction. Defaults to InMemoryEventQueue() (capacity from its own default config, 1024). See Event Queue doc. |
router |
Optional[Router] |
None |
Internal router / dispatch table. Defaults to Router(event_registry); exposed as bus.router. See Router doc. |
max_handler_semaphore |
int |
256 |
Max concurrent handlers (semaphore). |
shutdown |
ShutdownConfig |
ShutdownConfig() |
Shutdown behavior config. |
middleware_chain |
Optional[MiddlewareChain] |
None |
Middleware chain for publish hooks. |
Constructing automatically registers ShutdownEvent and TaskErrorEvent (if not present),
and creates an internal Router (exposed as bus.router) for event-to-handler
routing — handlers register their subscription bundles on activation (bus start). Queue capacity and
other settings live on the queue itself — the bus never knows them; inject
InMemoryEventQueue(InMemoryEventQueueConfig(maxsize=N)) to control capacity.
Lifecycle¶
| Method | Description |
|---|---|
start() |
Starts the event bus: first initializes the middleware chain (each middleware's on_setup), then activates the handler registry (registry.activate(bus) — each handler's subscription bundle registers into bus.router), and finally starts the dispatch loop and enables publishing — the order mirrors stop()'s cleanup, so messages published during handler activation go through a fully-ready middleware pipeline. Failure-safe & retryable: any failure rolls back (cancel dispatch, deactivate handlers, tear down middleware) to a clean not-started state. Idempotent; start failures always raise a BusError subclass — BusStartFailed when the rollback is complete (carries original_error and the __cause__ chain), or BusStartRollbackError when the rollback itself is incomplete (aggregates the original exception with the handler / middleware failure lists). |
stop() |
Graceful shutdown: publish __shutdown__ → reject new publishes → drain queue → cancel dispatch → wait for active tasks → deactivate the handler registry (registry.deactivate()); handler deactivation failures are logged and summarized. Idempotent. |
async with EventBus(...) as bus: |
Context manager, auto start/stop. stop() errors on exit won't mask body exceptions. |
Observability¶
| Property | Type | Description |
|---|---|---|
is_running |
bool |
Whether the bus is running. See is_running. |
is_publishing_enabled |
bool |
Whether new events are accepted. See is_publishing_enabled. |
active_task_count |
int |
Number of currently active handler tasks. |
queue_size |
int |
Number of events currently queued for dispatch. |
is_running¶
Indicates the event bus's running state. Its value changes over the lifecycle as follows:
| Phase | is_running |
Description |
|---|---|---|
After construction, before start() |
False |
Bus created but not yet started. |
During start() |
False |
Initializing middleware, activating handlers and creating the dispatch loop; not yet marked as running. |
After start() completes |
True |
Dispatch loop started, actively dispatching events. |
During stop() |
True |
Draining queue, waiting for active tasks to complete. |
After stop() completes |
False |
All resources released. |
Key point:
is_runningremainsTruethroughout the entire drain and wait period ofstop(), only becomingFalseafter all active tasks complete and middleware teardown finishes. This means during shutdown,is_running=Truebutis_publishing_enabled=False(see below).
bus = EventBus(reg, h_reg)
print(bus.is_running) # False
await bus.start()
print(bus.is_running) # True
# During stop(): draining queue + waiting on tasks, is_running remains True
await bus.stop()
print(bus.is_running) # False
is_publishing_enabled¶
Indicates whether new events can be published to the bus. Its value changes as follows:
| Phase | is_publishing_enabled |
Description |
|---|---|---|
After construction, before start() |
False |
Publishing raises RuntimeError. |
During start() |
False |
Middleware & handler initialization in progress; publishing not yet enabled. |
After start() completes |
True |
Normal publishing allowed. |
After stop() publishes __shutdown__ |
False |
New events rejected; publishing raises BusShuttingDown. |
After stop() completes |
False |
— |
Key point:
is_publishing_enabledis cleared immediately whenstop()begins (right after the__shutdown__event is published), before queue draining and active task waiting. This ensures no new events enter the queue during shutdown, while already-enqueued events are still dispatched normally.
Difference from is_running¶
| Scenario | is_running |
is_publishing_enabled |
|---|---|---|
| Normal operation | True |
True |
| Shutting down (draining queue, waiting tasks) | True |
False |
| Not started / Stopped | False |
False |
These two properties have state separation during shutdown: is_running stays True
to ensure the queue and tasks are fully processed, while is_publishing_enabled becomes
False early to block new events from entering.
Usage Scenarios¶
# Scenario 1: Health check endpoint
@app.get("/health")
async def health():
return {
"status": "ok" if bus.is_running else "degraded",
"can_publish": bus.is_publishing_enabled,
"queue_depth": bus.queue_size,
"active_handlers": bus.active_task_count,
}
# Scenario 2: Wait for bus to fully stop during graceful shutdown
async def graceful_shutdown():
signal.alarm("shutdown")
await bus.stop()
# At this point is_running == False, safe to exit the process
assert not bus.is_running
# Scenario 3: Pre-publish check (usually not needed; publish raises appropriate exceptions)
async def safe_publish(bus, name, data):
if not bus.is_publishing_enabled:
logger.warning("Bus no longer accepting new events, skipping publish")
return
await bus.proxy("my_service").publish(name, data)
State Transition Sequence Diagram¶
start() stop()
│ │
│ dispatch_task created │ 1. publish __shutdown__
│ _running.set() ◄── running ───► _running.clear()
│ _enable_publish.set() ◄── publish ───► _enable_publish.clear()
│ │
│ mw_chain.setup() │ 2. drain queue (queue.join())
│ │ 3. cancel dispatch_task
▼ ▼ 4. wait active tasks
is_running=True │ 5. mw_chain.teardown()
is_publishing_enabled=True ▼
is_running=False
is_publishing_enabled=False
EventBus.Proxy¶
The proxy is the only interface for publishing events. It auto-records event source and chain.
class Proxy:
async def publish(
self,
name: str,
data: Optional[Union[Dict[str, Any], BaseModel]] = None
) -> None
@property
def handlers_registry(self) -> EventHandlerRegistry
@property
def events_registry(self) -> EventRegistry
@property
def middleware(self) -> MiddlewareChain
| Member | Description |
|---|---|
publish(name, data=None) |
Publish an event. data can be a dict or pydantic model. Raises RuntimeError if not started, BusShuttingDown if stopping, ValueError for unknown events, TypeError for payload mismatch. |
handlers_registry |
Read-only access to handler registry. |
events_registry |
Read-only access to event registry. |
middleware |
Access middleware chain for runtime modification. |
Usage¶
# Basic publish
await bus.proxy("my_service").publish("user.login", {"user_id": "42"})
# Chained publish (event forwarding)
class LoginHandler(EventHandler):
async def handle(self, payload, bus_proxy, raw_event):
await bus_proxy.publish("user.profile_loaded", profile_data)
ShutdownConfig¶
Controls graceful shutdown timeout behavior.
class ShutdownConfig(BaseModel):
queue_timeout_min: float = 1.0
queue_timeout_max: float = 15.0
tasks_timeout: float = 15.0
task_reclaim_delay: float = 0.0
avg_wait_time: float = 0.05
| Field | Type | Default | Description |
|---|---|---|---|
queue_timeout_min |
float |
1.0 |
Minimum queue drain wait (seconds). |
queue_timeout_max |
float |
15.0 |
Maximum queue drain wait (seconds). |
tasks_timeout |
float |
15.0 |
Active handler task completion wait (seconds). |
task_reclaim_delay |
float |
0.0 |
Extra settle time before task reclamation, letting completion callbacks (and task-error logs) run first (seconds); 0 yields the event loop once. |
avg_wait_time |
float |
0.05 |
Estimated per-event processing time (seconds), used for dynamic queue drain timeout calculation. |
Actual queue drain timeout = max(queue_timeout_min, min(queue_timeout_max, queue_size × avg_wait_time)).
Exceptions¶
| Exception | When |
|---|---|
BusError |
Base class for all Bus* exceptions below. |
BusShuttingDown |
Publish attempted while bus is stopping. Callers should catch and perform cleanup. |
BusStartFailed |
Start failed with a complete rollback; carries original_error (also chained via __cause__). |
BusStartRollbackError |
Start failed and the rollback itself was incomplete; aggregates original_error, handler_failures, and middleware_failures. |
RuntimeError |
Publish attempted before bus has started. |
ValueError |
Unknown event name, or payload mismatch (required vs. provided). |
TypeError |
Payload type doesn't match the event declaration. |
Built-in Events¶
ShutdownEvent¶
class ShutdownEvent(EventDeclaration):
name = 'infinity_bus.__shutdown__'
# payload_type = None (no payload)
Published automatically when stop() is called, before queue draining begins.
Handlers can subscribe to this event to perform cleanup before shutdown.
TaskErrorEvent¶
class TaskErrorPayload(BaseModel):
error_event: Event
handler_id: Optional[str]
handler_name: str
error_type: str
error_message: str
class TaskErrorEvent(EventDeclaration):
name = 'infinity_bus.__task_error__'
payload_type = TaskErrorPayload
Published when a handler raises an exception. Source is always "EventBusErrorReporter".
Use this for centralized error monitoring.
class ErrorMonitor(EventHandler):
def __init__(self):
super().__init__(["infinity_bus.__task_error__"])
async def handle(self, payload, bus_proxy, raw_event):
if isinstance(payload, TaskErrorPayload):
logger.error(
"Handler %s failed on %s: %s",
payload.handler_name,
payload.error_event.name,
payload.error_message,
)
Usage Examples¶
Basic Assembly & Startup¶
from infinity_bus import EventBus, EventRegistry, EventHandlerRegistry
# 1. Declare events
class MyPayload(BaseModel):
message: str
class MyEvent(EventDeclaration):
name = "my.event"
payload_type = MyPayload
# 2. Register events
reg = EventRegistry()
reg.register(MyEvent)
# 3. Implement handler
class MyHandler(EventHandler):
def __init__(self):
super().__init__(subscriptions=["my.event"])
async def handle(self, payload, bus_proxy, raw_event):
print(f"Received: {payload.message}")
# 4. Register handler
h_reg = EventHandlerRegistry()
await h_reg.register(MyHandler())
# 5. Start bus and publish
async with EventBus(reg, h_reg) as bus:
await bus.proxy("cli").publish("my.event", {"message": "Hello"})
await asyncio.sleep(0.1) # Wait for handler output
Chained Publishing¶
Handlers can publish new events via bus_proxy.publish to form processing chains;
the bus automatically tracks sources:
class OrderHandler(EventHandler):
def __init__(self):
super().__init__(subscriptions=["order.created"])
async def handle(self, payload, bus_proxy, raw_event):
# Process order...
# Chain-publish a notification event
await bus_proxy.publish("notification.send", {
"type": "order_confirmed",
"order_id": payload.order_id
})