"""Server-side materialization for time and state driven business reminders.""" from __future__ import annotations import asyncio import logging from sqlalchemy import select, text from app.core.config import settings from app.db.session import SessionLocal from app.models.study_member import StudyMember from app.models.user import User, UserStatus from app.services.project_reminder_service import sync_project_reminders logger = logging.getLogger("ctms.notifications") NOTIFICATION_SYNC_LOCK_KEY = 0x43544D53 # "CTMS" async def sync_all_project_reminders_once() -> int: """Synchronize every active project member once, with a cross-worker advisory lock.""" async with SessionLocal() as lock_session: acquired = bool(await lock_session.scalar( text("SELECT pg_try_advisory_lock(:lock_key)"), {"lock_key": NOTIFICATION_SYNC_LOCK_KEY}, )) if not acquired: return 0 try: memberships = (await lock_session.execute( select(StudyMember.study_id, StudyMember.user_id) .join(User, User.id == StudyMember.user_id) .where( StudyMember.is_active.is_(True), User.status == UserStatus.ACTIVE, ) )).all() await lock_session.commit() synced = 0 for study_id, user_id in memberships: async with SessionLocal() as session: try: user = await session.get(User, user_id) if user is None or not user.is_active: continue await sync_project_reminders(session, study_id, user) synced += 1 except Exception: await session.rollback() logger.exception( "Failed to synchronize reminders for study=%s user=%s", study_id, user_id, ) return synced finally: await lock_session.execute( text("SELECT pg_advisory_unlock(:lock_key)"), {"lock_key": NOTIFICATION_SYNC_LOCK_KEY}, ) async def run_notification_sync_job(stop_event: asyncio.Event) -> None: logger.info("Notification synchronization task started") while not stop_event.is_set(): try: synced = await sync_all_project_reminders_once() logger.debug("Synchronized reminders for %s project memberships", synced) except Exception: logger.exception("Notification synchronization task failed") try: await asyncio.wait_for( stop_event.wait(), timeout=settings.NOTIFICATION_SYNC_INTERVAL_SECONDS, ) except asyncio.TimeoutError: continue logger.info("Notification synchronization task stopped")