Download tests/test_cache_circuit.py from sitcod3/modul-x: direct link, hf CLI and curl.
- Browser
- Download file 4.32 kB
-
https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/tests/test_cache_circuit.py
- Command line
-
hf download hf://spaces/sitcod3/modul-x/tests/test_cache_circuit.py
-
curl -L -o test_cache_circuit.py https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/tests/test_cache_circuit.py
4.32 kB
| import asyncio | |
| import time | |
| import unittest | |
| from unittest.mock import AsyncMock, patch | |
| from app.utils import cache | |
| from app.utils.cache import CircuitBreaker, get_cache, set_cache | |
| class CircuitBreakerLogicTests(unittest.TestCase): | |
| """Pure state machine, no I/O.""" | |
| def test_closed_initially_allows_traffic(self): | |
| b = CircuitBreaker(failure_threshold=3, cooldown_seconds=30) | |
| self.assertTrue(b.allow()) | |
| def test_opens_after_consecutive_failures(self): | |
| b = CircuitBreaker(failure_threshold=3, cooldown_seconds=30) | |
| for _ in range(3): | |
| b.record(success=False, duration=2.5) | |
| self.assertFalse(b.allow(), "circuit must open after threshold failures") | |
| def test_success_resets_the_failure_count(self): | |
| b = CircuitBreaker(failure_threshold=3, cooldown_seconds=30) | |
| b.record(success=False, duration=2.5) | |
| b.record(success=False, duration=2.5) | |
| b.record(success=True, duration=0.05) # healthy again | |
| b.record(success=False, duration=2.5) | |
| self.assertTrue(b.allow(), "one success must clear prior failures") | |
| def test_slow_op_counts_as_a_failure(self): | |
| # A 2.5s op that eventually succeeds is still a latency failure here. | |
| b = CircuitBreaker(failure_threshold=2, cooldown_seconds=30, | |
| slow_threshold=1.0) | |
| b.record(success=True, duration=2.5) | |
| b.record(success=True, duration=2.5) | |
| self.assertFalse(b.allow()) | |
| def test_half_open_after_cooldown(self): | |
| b = CircuitBreaker(failure_threshold=1, cooldown_seconds=0.05) | |
| b.record(success=False, duration=2.5) | |
| self.assertFalse(b.allow()) | |
| time.sleep(0.06) | |
| self.assertTrue(b.allow(), "cooldown expiry must allow a probe") | |
| class CacheCircuitTests(unittest.IsolatedAsyncioTestCase): | |
| """The cache must fail FAST to memory when Redis is slow, not per-call.""" | |
| async def test_slow_redis_does_not_stall_every_call(self): | |
| cache._redis_client = None | |
| cache._circuit.reset() | |
| class SlowRedis: | |
| async def get(self, key): | |
| await asyncio.sleep(2.0) | |
| return None | |
| async def setex(self, key, ttl, value): | |
| await asyncio.sleep(2.0) | |
| with patch.object(cache, "get_redis", AsyncMock(return_value=SlowRedis())): | |
| t0 = time.perf_counter() | |
| for _ in range(5): | |
| await get_cache("some:key") | |
| first = time.perf_counter() - t0 | |
| # Circuit opens after threshold -> later calls skip Redis entirely. | |
| for _ in range(5): | |
| await get_cache("some:key") | |
| later = time.perf_counter() - t0 - first | |
| self.assertLess( | |
| later, 1.0, | |
| f"open circuit still paid Redis latency ({later:.2f}s for 5 calls)", | |
| ) | |
| async def test_falls_back_to_memory_when_circuit_open(self): | |
| cache._circuit.reset() | |
| # Trip the breaker at the default threshold of 3. | |
| for _ in range(3): | |
| cache._circuit.record(success=False, duration=2.5) | |
| self.assertFalse(cache._circuit.allow(), "circuit should be open") | |
| with patch.object(cache, "get_redis", | |
| AsyncMock(side_effect=AssertionError("must not be called"))): | |
| await set_cache("mem:key", "value", 60) | |
| got = await get_cache("mem:key") | |
| self.assertEqual(got, "value", "memory fallback must still work") | |
| async def test_healthy_redis_is_used_normally(self): | |
| cache._redis_client = "sentinel" | |
| cache._circuit.reset() | |
| class FastRedis: | |
| def __init__(self): | |
| self.store = {} | |
| async def get(self, key): | |
| return self.store.get(key) | |
| async def setex(self, key, ttl, value): | |
| self.store[key] = value | |
| fast = FastRedis() | |
| with patch.object(cache, "get_redis", AsyncMock(return_value=fast)): | |
| await set_cache("fast:key", "v1", 60) | |
| got = await get_cache("fast:key") | |
| self.assertEqual(got, "v1") | |
| self.assertTrue(cache._circuit.allow()) | |
| async def tearDown(self): | |
| cache._redis_client = None | |
| cache._circuit.reset() | |
| if __name__ == "__main__": | |
| unittest.main() |