""" Runtime feature flags for the vision pipeline. Only one flag now — the master vision switch. Runtime overrides live in Redis under ``mulita:flags:``; an unset key falls back to the YAML default. """ from __future__ import annotations import logging from typing import Optional import redis from app.config import settings logger = logging.getLogger(__name__) FLAG_VISION_ENABLED = 'vision.enabled' ALL_FLAGS = (FLAG_VISION_ENABLED,) _VISION_QUEUE = 'vision' _REDIS: Optional[redis.Redis] = None def _redis() -> Optional[redis.Redis]: global _REDIS if _REDIS is None: try: _REDIS = redis.Redis.from_url( settings.celery_broker_url, decode_responses=True ) _REDIS.ping() except Exception as e: logger.warning(f"feature_flags: Redis unavailable, using YAML defaults ({e})") _REDIS = None return _REDIS def _yaml_default(name: str) -> bool: if name == FLAG_VISION_ENABLED: return bool(settings.vision.enabled) raise ValueError(f"Unknown feature flag: {name!r}") def _redis_key(name: str) -> str: return f"mulita:flags:{name}" def is_enabled(name: str) -> bool: r = _redis() if r is not None: try: raw = r.get(_redis_key(name)) if raw is not None: return raw.lower() == 'true' except Exception as e: logger.warning(f"feature_flags: Redis read failed for {name} ({e})") return _yaml_default(name) def set_flag(name: str, value: bool) -> None: if name not in ALL_FLAGS: raise ValueError(f"Unknown feature flag: {name!r}") r = _redis() if r is None: raise RuntimeError("Redis unavailable; cannot update feature flags") r.set(_redis_key(name), 'true' if value else 'false') _apply_worker_side_effects(name) def reset_flag(name: str) -> None: if name not in ALL_FLAGS: raise ValueError(f"Unknown feature flag: {name!r}") r = _redis() if r is None: raise RuntimeError("Redis unavailable; cannot reset feature flags") r.delete(_redis_key(name)) _apply_worker_side_effects(name) def _apply_worker_side_effects(name: str) -> None: """Attach or detach the vision consumer and purge queued work when the master flag flips. Best-effort — state is already persisted.""" if name != FLAG_VISION_ENABLED: return try: from app.tasks.celery import celery_app except Exception as e: logger.warning(f"feature_flags: celery app unavailable for side effects ({e})") return try: if is_enabled(FLAG_VISION_ENABLED): celery_app.control.add_consumer(_VISION_QUEUE, reply=False) logger.info("feature_flags: vision re-enabled; consumer added") else: celery_app.control.cancel_consumer(_VISION_QUEUE, reply=False) _purge_queue(_VISION_QUEUE) logger.info("feature_flags: vision disabled; consumer cancelled and queue purged") except Exception as e: logger.warning(f"feature_flags: worker side effects failed: {e}") def _purge_queue(queue: str) -> int: r = _redis() if r is None: return 0 try: return int(r.delete(queue) or 0) except Exception as e: logger.warning(f"feature_flags: purge {queue} failed: {e}") return 0 def snapshot() -> dict[str, dict[str, object]]: r = _redis() out: dict[str, dict[str, object]] = {} for name in ALL_FLAGS: default = _yaml_default(name) override = None if r is not None: try: raw = r.get(_redis_key(name)) if raw is not None: override = raw.lower() == 'true' except Exception: pass out[name] = { 'effective': override if override is not None else default, 'default': default, 'overridden': override is not None, } return out