"""Shared low-level access to the `chaos:flags` Redis hash. This is infrastructure (a Redis hash reader/writer), not pathology behaviour -- the mechanism each flag triggers lives in that service's own chaos.py, per phase-0-base-project-spec.md §7 ("each pathology lives in a clearly named chaos.py module inside its owning service"). Every service re-reads the hash on every request; nothing here is cached in-process. """ import random import time from .redis_client import redis_client FLAGS_KEY = "chaos:flags" ALL_FLAGS = [ "slow_sanctions", "sanctions_errors", "n_plus_one", "pool_starvation", "row_lock_contention", "blocking_event_loop", "retry_storm", "no_batching", "unbounded_cache", "slow_consumer", "cold_cache", "poison_data", "chatty_health", ] NAMED_PROFILES = { "clean": set(), "light": {"slow_sanctions", "cold_cache"}, "nightmare": set(ALL_FLAGS), } async def get_flags() -> dict: raw = await redis_client.hgetall(FLAGS_KEY) return {flag: raw.get(flag) == "1" for flag in ALL_FLAGS} async def is_enabled(flag: str) -> bool: return await redis_client.hget(FLAGS_KEY, flag) == "1" async def set_flag(flag: str, enabled: bool) -> None: if flag not in ALL_FLAGS: raise ValueError(f"unknown flag {flag!r}") await redis_client.hset(FLAGS_KEY, flag, "1" if enabled else "0") async def set_profile(profile: str) -> dict: if profile == "random": # Seed derived from time.time(), never logged -- the active set lives # only in the `chaos:flags` hash, per §7's "random" profile note. rng = random.Random(time.time()) chosen = set(rng.sample(ALL_FLAGS, k=rng.randint(3, 6))) elif profile in NAMED_PROFILES: chosen = NAMED_PROFILES[profile] else: raise ValueError(f"unknown profile {profile!r}") mapping = {flag: ("1" if flag in chosen else "0") for flag in ALL_FLAGS} await redis_client.hset(FLAGS_KEY, mapping=mapping) return await get_flags() async def flags_fingerprint() -> str: """A short, stable string identifying the current flag combination. Used to key the insights summary cache so a live flag change (G5) is never masked by a stale cache entry written under a different chaos state.""" flags = await get_flags() return "".join("1" if flags[f] else "0" for f in ALL_FLAGS)