727d86614e
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""In-process event bus for publish/subscribe.
|
|
|
|
.. note::
|
|
|
|
This bus is **in-process only** — events are lost on crash, restart, or
|
|
when multiple replicas are running. For **domain/business events** that
|
|
must be delivered reliably (e.g. ``contact.created``, ``contact.updated``,
|
|
``user.created``), use the :mod:`app.core.outbox` transactional outbox
|
|
instead::
|
|
|
|
from app.core.outbox import enqueue_outbox_event
|
|
await enqueue_outbox_event(db, tenant_id, "contact.created", {...})
|
|
|
|
The outbox worker (see :mod:`app.core.worker`) polls the ``event_outbox``
|
|
table every 5 seconds and publishes events to this in-process bus, so
|
|
local handlers still receive them — but with durability guarantees.
|
|
|
|
``publish()`` may still be used for **uncritical local events** that do
|
|
not require persistence (e.g. cache invalidation signals).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections import defaultdict
|
|
from collections.abc import Callable, Coroutine
|
|
from typing import Any
|
|
|
|
EventHandler = Callable[[dict[str, Any]], Coroutine[Any, Any, None]]
|
|
|
|
|
|
class EventBus:
|
|
"""Simple async event bus for in-process pub/sub."""
|
|
|
|
def __init__(self) -> None:
|
|
self._handlers: dict[str, list[EventHandler]] = defaultdict(list)
|
|
|
|
def subscribe(self, event_name: str, handler: EventHandler) -> None:
|
|
"""Subscribe a handler to an event."""
|
|
self._handlers[event_name].append(handler)
|
|
|
|
def unsubscribe(self, event_name: str, handler: EventHandler) -> None:
|
|
"""Unsubscribe a handler from an event."""
|
|
if event_name in self._handlers:
|
|
self._handlers[event_name] = [h for h in self._handlers[event_name] if h is not handler]
|
|
|
|
async def publish(self, event_name: str, payload: dict[str, Any]) -> None:
|
|
"""Publish an event to all subscribers."""
|
|
handlers = self._handlers.get(event_name, [])
|
|
# Also notify wildcard subscribers (catch-all '*' handlers)
|
|
wildcard_handlers = self._handlers.get('*', [])
|
|
all_handlers = handlers + wildcard_handlers
|
|
tasks = [asyncio.create_task(h(payload)) for h in all_handlers]
|
|
if tasks:
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
async def publish_with_results(
|
|
self, event_name: str, payload: dict[str, Any]
|
|
) -> list[Exception | None]:
|
|
"""Publish an event and return per-handler results.
|
|
|
|
Unlike :meth:`publish`, this method does **not** swallow exceptions.
|
|
Each list entry is ``None`` on success or the caught ``Exception``
|
|
on failure, so callers (e.g. the outbox processor) can detect handler
|
|
errors and apply retry logic.
|
|
"""
|
|
handlers = self._handlers.get(event_name, [])
|
|
wildcard_handlers = self._handlers.get('*', [])
|
|
all_handlers = handlers + wildcard_handlers
|
|
if not all_handlers:
|
|
return []
|
|
tasks = [asyncio.create_task(h(payload)) for h in all_handlers]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
return [
|
|
r if isinstance(r, Exception) else None for r in results
|
|
]
|
|
|
|
|
|
# Global event bus instance
|
|
_event_bus = EventBus()
|
|
|
|
|
|
def get_event_bus() -> EventBus:
|
|
"""Get the global event bus."""
|
|
return _event_bus
|
|
|
|
|
|
def register_workflow_event_handlers() -> None:
|
|
"""Register workflow event handlers on the global event bus.
|
|
|
|
Subscribes to events that can trigger workflows (user.created, etc.).
|
|
Should be called during application startup.
|
|
"""
|
|
from app.workflows.engine import register_workflow_event_handlers as _register
|
|
|
|
_register()
|