"""Redis Pub/Sub — sync in-memory settings across uvicorn workers. When one worker handles PUT /settings/{key}, it hot-reloads locally and publishes to nexus:settings so other workers apply the same override. """ import asyncio import json import logging from typing import Optional logger = logging.getLogger("nexus.settings_broadcast") REDIS_SETTINGS_CHANNEL = "nexus:settings" _subscriber_task: Optional[asyncio.Task] = None _redis_subscriber = None def apply_setting_override(db_key: str, value: str) -> bool: """Apply a single DB settings key to the in-memory Settings singleton.""" from server.config import settings return settings.apply_db_override(db_key, value) async def publish_setting_change(db_key: str, value: str) -> bool: """Broadcast a settings change to all workers via Redis Pub/Sub.""" try: from server.infrastructure.redis.client import get_redis redis = get_redis() payload = json.dumps({"key": db_key, "value": value}) await redis.publish(REDIS_SETTINGS_CHANNEL, payload) return True except Exception as exc: logger.warning("Settings broadcast publish failed for %s: %s", db_key, exc) return False async def publish_setting_changes(changes: dict[str, str]) -> int: """Apply in-memory overrides and broadcast each key to all workers. Returns the number of keys successfully published. """ from server.config import settings published = 0 for db_key, value in changes.items(): if settings.apply_db_override(db_key, value): if await publish_setting_change(db_key, value): published += 1 return published async def start_settings_subscriber() -> None: """Subscribe to nexus:settings — called during app startup (every worker).""" global _subscriber_task, _redis_subscriber try: from server.infrastructure.redis.client import get_redis redis = get_redis() _redis_subscriber = redis.pubsub() await _redis_subscriber.subscribe(REDIS_SETTINGS_CHANNEL) _subscriber_task = asyncio.create_task(_listen_loop(), name="settings_redis_sub") logger.info("Settings Pub/Sub subscriber started on channel: %s", REDIS_SETTINGS_CHANNEL) except Exception as exc: logger.error("Settings Pub/Sub subscriber failed to start: %s", exc) async def stop_settings_subscriber() -> None: """Stop settings subscriber — called during app shutdown.""" global _subscriber_task, _redis_subscriber if _subscriber_task: _subscriber_task.cancel() try: await _subscriber_task except asyncio.CancelledError: pass _subscriber_task = None if _redis_subscriber: try: await _redis_subscriber.unsubscribe(REDIS_SETTINGS_CHANNEL) await _redis_subscriber.aclose() except Exception: logger.debug("Settings Pub/Sub unsubscribe failed during shutdown", exc_info=True) _redis_subscriber = None logger.info("Settings Pub/Sub subscriber stopped") async def _listen_loop() -> None: while True: try: message = await _redis_subscriber.get_message( ignore_subscribe_messages=True, timeout=1.0 ) if not message or message.get("type") != "message": continue data = message.get("data") if isinstance(data, bytes): data = data.decode() if not isinstance(data, str): continue payload = json.loads(data) db_key = payload.get("key") value = payload.get("value") if db_key is None or value is None: continue if apply_setting_override(str(db_key), str(value)): logger.debug("Settings broadcast applied: %s", db_key) except asyncio.CancelledError: break except Exception as exc: logger.error("Settings Pub/Sub listen error: %s", exc) await asyncio.sleep(1)