Split databases and Redis — prepare infrastructure for per-domain isolation
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m20s

Redis: per-app DB index (0-5) with shared auth DB 15 for SSO keys;
flushdb replaces flushall so deploys don't wipe cross-app auth state.

Postgres: drop 13 cross-domain FK constraints (migration v2t0p8q9r0),
remove dead ORM relationships, add explicit joins for 4 live ones.
Multi-engine sessions (account + federation) ready for per-domain DBs
via DATABASE_URL_ACCOUNT / DATABASE_URL_FEDERATION env vars.

All URLs initially point to the same appdb — zero behaviour change
until split-databases.sh is run to migrate data to per-domain DBs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-25 02:20:34 +00:00
parent 57d2a6a6e3
commit 580f551700
25 changed files with 459 additions and 102 deletions

View File

@@ -4,6 +4,11 @@ Unified activity bus.
emit_activity() writes an APActivity row with process_state='pending' within
the caller's existing DB transaction — atomic with the domain change.
When the federation database is separate (DATABASE_URL_FEDERATION differs from
DATABASE_URL), emit_activity() opens its own federation session and commits
independently. Atomicity is traded for domain isolation; handlers are
idempotent, so at-least-once delivery is safe.
register_activity_handler() registers async handler functions that the
EventProcessor dispatches when processing pending activities.
"""
@@ -73,6 +78,12 @@ def get_activity_handlers(
return handlers
def _needs_federation_session() -> bool:
"""True when the federation DB differs from the app's default DB."""
from shared.db.session import DATABASE_URL, DATABASE_URL_FEDERATION
return DATABASE_URL_FEDERATION != DATABASE_URL
# ---------------------------------------------------------------------------
# emit_activity — the primary way to emit events
# ---------------------------------------------------------------------------
@@ -92,8 +103,10 @@ async def emit_activity(
"""
Write an AP-shaped activity to ap_activities with process_state='pending'.
Called inside a service function using the same session that performs the
domain change. The activity and the change commit together.
When all apps share one database the activity is written in the caller's
transaction (atomic with the domain change). When the federation DB is
separate, a dedicated federation session is used and committed
independently.
"""
if not origin_app:
try:
@@ -118,9 +131,17 @@ async def emit_activity(
process_state="pending",
origin_app=origin_app,
)
session.add(activity)
await session.flush()
# Wake any listening EventProcessor as soon as this transaction commits.
# NOTIFY is transactional — delivered only after commit.
await session.execute(text("NOTIFY ap_activity_pending"))
if _needs_federation_session():
from shared.db.session import get_federation_session
async with get_federation_session() as fed_s:
async with fed_s.begin():
fed_s.add(activity)
await fed_s.flush()
await fed_s.execute(text("NOTIFY ap_activity_pending"))
else:
session.add(activity)
await session.flush()
await session.execute(text("NOTIFY ap_activity_pending"))
return activity