76 lines
2.3 KiB
Python
76 lines
2.3 KiB
Python
"""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)
|