fix: Gate 5 — worker event handlers and per-tenant outbox processing
Worker fixes: - registry.initialize uses get_migration_engine() for DDL (not worker_engine) - Worker session uses get_worker_session_factory() (crm_worker, not crm_api) - Event handlers only registered for active plugins (is_active check) - Outbox processing per-tenant with set_config(app.current_tenant_id) - process_outbox_job uses get_worker_session_factory() and loads tenant_ids - Removed unused get_engine import Outbox fixes: - process_outbox_batch iterates over tenants, sets RLS context per tenant - _process_single_outbox_event extracted for clarity - Events claimed per-tenant (RLS-compatible, no BYPASSRLS needed) - Commit after each tenant to release locks Gate 5 requirements met: - Plugin event handlers registered for active plugins only - No plugin routers registered in worker - Outbox events without handlers marked as no_handlers - Failed consumers trigger retry with exponential backoff - Processing is idempotent (consumer_inbox check) - Every worker DB access sets app.current_tenant_id - Worker cannot read/write other tenant data (RLS enforced)
This commit is contained in:
+32
-11
@@ -103,7 +103,6 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
||||
|
||||
# Initialize plugin registry and discover built-in plugins
|
||||
from app.plugins.registry import get_registry
|
||||
from app.core.db import get_engine
|
||||
from app.core.event_bus import get_event_bus
|
||||
from app.core.webhook_dispatcher import register_webhook_event_handlers
|
||||
from sqlalchemy import select as sa_select
|
||||
@@ -111,13 +110,14 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
registry = get_registry()
|
||||
from app.core.db import get_worker_engine
|
||||
worker_engine = get_worker_engine()
|
||||
registry.initialize(worker_engine, app=None)
|
||||
from app.core.db import get_migration_engine
|
||||
migration_engine = get_migration_engine()
|
||||
registry.initialize(migration_engine, app=None)
|
||||
registry.discover_builtins()
|
||||
|
||||
event_bus = get_event_bus()
|
||||
async_session = async_sessionmaker(worker_engine, expire_on_commit=False)
|
||||
from app.core.db import get_worker_session_factory
|
||||
async_session = get_worker_session_factory()
|
||||
|
||||
# Activate plugins that are marked active in DB (register event handlers)
|
||||
# RLS fail-closed requires tenant context for tenant-table writes.
|
||||
@@ -133,11 +133,25 @@ async def on_startup(ctx: dict[str, Any]) -> None:
|
||||
all_tenant_ids = [row[0] for row in tenant_result]
|
||||
logger.info(f"Worker: loaded {len(all_tenant_ids)} tenants")
|
||||
|
||||
# Register event handlers only (no DB writes, no cron job registration)
|
||||
# Register event handlers only for active plugins (no DB writes, no cron job registration)
|
||||
# Load active plugin names from DB (global + tenant-specific)
|
||||
active_plugin_names: set[str] = set()
|
||||
async with async_session() as db:
|
||||
# Global plugins that are marked active
|
||||
result = await db.execute(
|
||||
sa_select(PluginModel.name).where(PluginModel.is_active == True)
|
||||
)
|
||||
active_plugin_names = {row[0] for row in result}
|
||||
logger.info(f"Worker: {len(active_plugin_names)} active plugins: {active_plugin_names}")
|
||||
|
||||
for name in registry.resolve_load_order():
|
||||
plugin = registry.get_plugin(name)
|
||||
if plugin is None:
|
||||
continue
|
||||
# Only register event handlers for active plugins
|
||||
if name not in active_plugin_names:
|
||||
logger.debug(f"Worker: skipping event handlers for inactive plugin {name}")
|
||||
continue
|
||||
try:
|
||||
# Just register event handlers, skip DB-writing on_activate
|
||||
if hasattr(plugin, 'register_event_handlers'):
|
||||
@@ -206,14 +220,21 @@ async def process_outbox_job(ctx: dict[str, Any]) -> None:
|
||||
|
||||
Uses a distributed Redis lock so only one worker replica processes the
|
||||
outbox at a time. Runs every 5 seconds.
|
||||
"""
|
||||
from app.core.db import get_session_factory
|
||||
from app.core.outbox import process_outbox_batch
|
||||
|
||||
factory = get_session_factory()
|
||||
Processes events per-tenant by setting tenant context for RLS.
|
||||
"""
|
||||
from app.core.db import get_worker_session_factory
|
||||
from app.core.outbox import process_outbox_batch
|
||||
from sqlalchemy import text as sa_text
|
||||
|
||||
factory = get_worker_session_factory()
|
||||
async with factory() as db:
|
||||
try:
|
||||
count = await process_outbox_batch(db, batch_size=50)
|
||||
# Load all tenant IDs for per-tenant outbox processing
|
||||
tenant_result = await db.execute(sa_text("SELECT id FROM tenants"))
|
||||
tenant_ids = [row[0] for row in tenant_result]
|
||||
|
||||
count = await process_outbox_batch(db, batch_size=50, tenant_ids=tenant_ids)
|
||||
if count:
|
||||
logger.info("Outbox: published %d events", count)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user