Coverage for fastblocks/actions/sync/cache.py: 29%
275 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Cache synchronization and consistency management across layers."""
3import typing as t
5from acb.debug import debug
7from .strategies import SyncResult, SyncStrategy
10class CacheSyncResult(SyncResult):
11 def __init__(
12 self,
13 *,
14 invalidated_keys: list[str] | None = None,
15 warmed_keys: list[str] | None = None,
16 cleared_namespaces: list[str] | None = None,
17 **kwargs: t.Any,
18 ) -> None:
19 super().__init__(**kwargs)
20 self.invalidated_keys = invalidated_keys if invalidated_keys is not None else []
21 self.warmed_keys = warmed_keys if warmed_keys is not None else []
22 self.cleared_namespaces = (
23 cleared_namespaces if cleared_namespaces is not None else []
24 )
27async def sync_cache(
28 *,
29 operation: str = "refresh",
30 namespaces: list[str] | None = None,
31 keys: list[str] | None = None,
32 warm_templates: bool = True,
33 strategy: SyncStrategy | None = None,
34) -> CacheSyncResult:
35 if strategy is None:
36 strategy = SyncStrategy()
38 if namespaces is None:
39 namespaces = ["templates", "bccache", "responses"]
41 debug(f"Starting cache sync: operation={operation}, namespaces={namespaces}")
43 result = CacheSyncResult()
45 try:
46 from acb.depends import depends
48 cache = depends.get("cache")
50 if not cache:
51 result.errors.append(Exception("Cache adapter not available"))
52 return result
54 except Exception as e:
55 result.errors.append(e)
56 return result
58 try:
59 if operation == "refresh":
60 await _refresh_cache(cache, namespaces, strategy, result)
61 elif operation == "invalidate":
62 await _invalidate_cache(cache, namespaces, keys, strategy, result)
63 elif operation == "warm":
64 await _warm_cache(cache, namespaces, strategy, result)
65 elif operation == "clear":
66 await _clear_cache(cache, namespaces, strategy, result)
67 else:
68 result.errors.append(ValueError(f"Unknown cache operation: {operation}"))
69 return result
71 if (
72 warm_templates
73 and operation in ("refresh", "invalidate")
74 and result.invalidated_keys
75 ):
76 await _warm_template_cache(cache, strategy, result)
78 except Exception as e:
79 result.errors.append(e)
80 debug(f"Error in cache sync operation {operation}: {e}")
82 debug(
83 f"Cache sync completed: {len(result.invalidated_keys)} invalidated, {len(result.warmed_keys)} warmed",
84 )
86 return result
89async def _refresh_cache(
90 cache: t.Any,
91 namespaces: list[str],
92 strategy: SyncStrategy,
93 result: CacheSyncResult,
94) -> None:
95 await _invalidate_cache(cache, namespaces, None, strategy, result)
97 await _warm_cache(cache, namespaces, strategy, result)
100async def _invalidate_cache(
101 cache: t.Any,
102 namespaces: list[str],
103 keys: list[str] | None,
104 strategy: SyncStrategy,
105 result: CacheSyncResult,
106) -> None:
107 if strategy.dry_run:
108 debug("DRY RUN: Would invalidate cache entries")
109 result.invalidated_keys.append("DRY_RUN_INVALIDATION")
110 return
112 if keys:
113 for key in keys:
114 try:
115 await cache.delete(key)
116 result.invalidated_keys.append(key)
117 debug(f"Invalidated cache key: {key}")
118 except Exception as e:
119 result.errors.append(e)
121 for namespace in namespaces:
122 try:
123 if namespace == "templates":
124 pattern = "template:*"
125 deleted_keys = await cache.delete_pattern(pattern)
126 result.invalidated_keys.extend(deleted_keys or [pattern])
127 debug(f"Invalidated template cache: {pattern}")
129 elif namespace == "bccache":
130 pattern = "bccache:*"
131 deleted_keys = await cache.delete_pattern(pattern)
132 result.invalidated_keys.extend(deleted_keys or [pattern])
133 debug(f"Invalidated bytecode cache: {pattern}")
135 elif namespace == "responses":
136 pattern = "response:*"
137 deleted_keys = await cache.delete_pattern(pattern)
138 result.invalidated_keys.extend(deleted_keys or [pattern])
139 debug(f"Invalidated response cache: {pattern}")
141 elif namespace == "gather":
142 pattern = "routes:*"
143 deleted_keys = await cache.delete_pattern(pattern)
144 result.invalidated_keys.extend(deleted_keys or [pattern])
146 pattern = "templates:*"
147 deleted_keys = await cache.delete_pattern(pattern)
148 result.invalidated_keys.extend(deleted_keys or [pattern])
150 pattern = "middleware:*"
151 deleted_keys = await cache.delete_pattern(pattern)
152 result.invalidated_keys.extend(deleted_keys or [pattern])
154 debug("Invalidated gather cache")
156 else:
157 pattern = f"{namespace}:*"
158 deleted_keys = await cache.delete_pattern(pattern)
159 result.invalidated_keys.extend(deleted_keys or [pattern])
160 debug(f"Invalidated namespace {namespace}: {pattern}")
162 except Exception as e:
163 result.errors.append(e)
164 debug(f"Error invalidating namespace {namespace}: {e}")
167async def _warm_cache(
168 cache: t.Any,
169 namespaces: list[str],
170 strategy: SyncStrategy,
171 result: CacheSyncResult,
172) -> None:
173 if strategy.dry_run:
174 debug("DRY RUN: Would warm cache entries")
175 result.warmed_keys.append("DRY_RUN_WARMING")
176 return
178 for namespace in namespaces:
179 try:
180 if namespace == "templates":
181 await _warm_template_cache(cache, strategy, result)
182 elif namespace == "responses":
183 await _warm_response_cache(cache, strategy, result)
184 elif namespace == "gather":
185 await _warm_gather_cache(cache, strategy, result)
187 except Exception as e:
188 result.errors.append(e)
189 debug(f"Error warming namespace {namespace}: {e}")
192async def _clear_cache(
193 cache: t.Any,
194 namespaces: list[str],
195 strategy: SyncStrategy,
196 result: CacheSyncResult,
197) -> None:
198 if strategy.dry_run:
199 debug("DRY RUN: Would clear cache namespaces")
200 result.cleared_namespaces.extend(namespaces)
201 return
203 for namespace in namespaces:
204 try:
205 await cache.clear(namespace)
206 result.cleared_namespaces.append(namespace)
207 debug(f"Cleared cache namespace: {namespace}")
209 except Exception as e:
210 result.errors.append(e)
211 debug(f"Error clearing namespace {namespace}: {e}")
214async def _warm_template_cache(
215 cache: t.Any,
216 strategy: SyncStrategy,
217 result: CacheSyncResult,
218) -> None:
219 common_templates = [
220 "base.html",
221 "layout.html",
222 "index.html",
223 "404.html",
224 "500.html",
225 "login.html",
226 "dashboard.html",
227 ]
229 try:
230 from acb.depends import depends
232 storage = depends.get("storage")
234 if not storage:
235 debug("Storage not available for template warming")
236 return
238 for template_path in common_templates:
239 try:
240 if await storage.templates.exists(template_path):
241 content = await storage.templates.read(template_path)
243 cache_key = f"template:{template_path}"
244 await cache.set(cache_key, content, ttl=86400)
245 result.warmed_keys.append(cache_key)
247 debug(f"Warmed template cache: {template_path}")
249 except Exception as e:
250 debug(f"Error warming template {template_path}: {e}")
252 except Exception as e:
253 debug(f"Error in template cache warming: {e}")
256async def _warm_response_cache(
257 cache: t.Any,
258 strategy: SyncStrategy,
259 result: CacheSyncResult,
260) -> None:
261 debug("Response cache warming not implemented - requires HTTP client")
264async def _warm_gather_cache(
265 cache: t.Any,
266 strategy: SyncStrategy,
267 result: CacheSyncResult,
268) -> None:
269 try:
270 from fastblocks.actions.gather import gather
272 try:
273 routes_result = await gather.routes()
274 if routes_result.total_routes > 0:
275 result.warmed_keys.append("gather:routes")
276 debug("Warmed gather routes cache")
277 except Exception as e:
278 debug(f"Error warming routes cache: {e}")
280 try:
281 templates_result = await gather.templates()
282 if templates_result.total_components > 0:
283 result.warmed_keys.append("gather:templates")
284 debug("Warmed gather templates cache")
285 except Exception as e:
286 debug(f"Error warming templates cache: {e}")
288 try:
289 middleware_result = await gather.middleware()
290 if middleware_result.total_middleware > 0:
291 result.warmed_keys.append("gather:middleware")
292 debug("Warmed gather middleware cache")
293 except Exception as e:
294 debug(f"Error warming middleware cache: {e}")
296 except Exception as e:
297 debug(f"Error warming gather cache: {e}")
300async def invalidate_template_cache(
301 template_paths: list[str] | None = None,
302 cache_namespace: str = "templates",
303) -> dict[str, t.Any]:
304 result: dict[str, t.Any] = {
305 "invalidated": [],
306 "errors": [],
307 }
309 try:
310 from acb.depends import depends
312 cache = depends.get("cache")
314 if not cache:
315 result["errors"].append("Cache adapter not available")
316 return result
318 if not template_paths:
319 pattern = f"{cache_namespace}:*"
320 deleted_keys = await cache.delete_pattern(pattern)
321 result["invalidated"].extend(deleted_keys or [pattern])
322 else:
323 for template_path in template_paths:
324 try:
325 template_key = f"{cache_namespace}:{template_path}"
326 await cache.delete(template_key)
327 result["invalidated"].append(template_key)
329 bytecode_key = f"bccache:{template_path}"
330 await cache.delete(bytecode_key)
331 result["invalidated"].append(bytecode_key)
333 await cache.delete_pattern(f"{cache_namespace}:*:{template_path}")
334 await cache.delete_pattern(f"bccache:*:{template_path}")
336 debug(f"Invalidated cache for template: {template_path}")
338 except Exception as e:
339 result["errors"].append(f"{template_path}: {e}")
341 except Exception as e:
342 result["errors"].append(str(e))
343 debug(f"Error invalidating template cache: {e}")
345 return result
348async def get_cache_stats(
349 namespaces: list[str] | None = None,
350) -> dict[str, t.Any]:
351 if namespaces is None:
352 namespaces = ["templates", "bccache", "responses", "gather"]
354 stats: dict[str, t.Any] = {
355 "total_keys": 0,
356 "namespaces": {},
357 "memory_usage": 0,
358 "hit_rate": 0.0,
359 "errors": [],
360 }
362 try:
363 cache = await _get_cache_adapter(stats)
364 if not cache:
365 return stats
367 await _collect_cache_info(cache, stats)
368 await _collect_namespace_stats(cache, namespaces, stats)
370 except Exception as e:
371 stats["errors"].append(str(e))
372 debug(f"Error getting cache stats: {e}")
374 return stats
377async def _get_cache_adapter(stats: dict[str, t.Any]) -> t.Any:
378 from acb.depends import depends
380 cache = depends.get("cache")
381 if not cache:
382 stats["errors"].append("Cache adapter not available")
383 return cache
386async def _collect_cache_info(cache: t.Any, stats: dict[str, t.Any]) -> None:
387 try:
388 info = await cache.info()
389 stats["memory_usage"] = info.get("used_memory", 0)
390 hits = info.get("keyspace_hits", 0)
391 misses = info.get("keyspace_misses", 0)
392 stats["hit_rate"] = hits / max(hits + misses, 1)
393 except Exception as e:
394 stats["errors"].append(f"Error getting cache info: {e}")
397async def _collect_namespace_stats(
398 cache: t.Any,
399 namespaces: list[str],
400 stats: dict[str, t.Any],
401) -> None:
402 for namespace in namespaces:
403 try:
404 pattern = f"{namespace}:*"
405 keys = await cache.keys(pattern)
406 key_count = len(keys) if keys else 0
408 stats["namespaces"][namespace] = {
409 "key_count": key_count,
410 "sample_keys": keys[:5] if keys else [],
411 }
413 stats["total_keys"] += key_count
415 except Exception as e:
416 stats["errors"].append(f"Error checking namespace {namespace}: {e}")
419async def optimize_cache(
420 max_memory_mb: int = 512,
421 eviction_policy: str = "allkeys-lru",
422) -> dict[str, t.Any]:
423 result: dict[str, t.Any] = {
424 "optimizations": [],
425 "warnings": [],
426 "errors": [],
427 }
429 try:
430 cache = await _get_cache_adapter(result)
431 if not cache:
432 result["errors"].append("Cache adapter not available")
433 return result
435 # Configure memory settings
436 await _configure_memory_settings(cache, max_memory_mb, eviction_policy, result)
438 # Analyze cache stats and generate warnings
439 await _analyze_cache_stats(result)
441 except Exception as e:
442 result["errors"].append(str(e))
443 debug(f"Error optimizing cache: {e}")
445 return result
448async def _configure_memory_settings(
449 cache: t.Any, max_memory_mb: int, eviction_policy: str, result: dict[str, t.Any]
450) -> None:
451 """Configure cache memory settings."""
452 try:
453 max_memory_bytes = max_memory_mb * 1024 * 1024
454 await cache.config_set("maxmemory", max_memory_bytes)
455 result["optimizations"].append(f"Set max memory to {max_memory_mb}MB")
456 except Exception as e:
457 result["errors"].append(f"Error setting max memory: {e}")
459 try:
460 await cache.config_set("maxmemory-policy", eviction_policy)
461 result["optimizations"].append(f"Set eviction policy to {eviction_policy}")
462 except Exception as e:
463 result["errors"].append(f"Error setting eviction policy: {e}")
466async def _analyze_cache_stats(result: dict[str, t.Any]) -> None:
467 """Analyze cache statistics and generate warnings."""
468 stats = await get_cache_stats()
470 if stats["hit_rate"] < 0.8:
471 result["warnings"].append(f"Low cache hit rate: {stats['hit_rate']:.2%}")
473 if stats["total_keys"] > 10000:
474 result["warnings"].append(f"High key count: {stats['total_keys']}")
476 for namespace, info in stats["namespaces"].items():
477 if info["key_count"] > 5000:
478 result["warnings"].append(
479 f"Namespace {namespace} has {info['key_count']} keys",
480 )
483def get_cache_sync_summary(result: CacheSyncResult) -> dict[str, t.Any]:
484 return {
485 "invalidated_count": len(result.invalidated_keys),
486 "warmed_count": len(result.warmed_keys),
487 "cleared_namespaces": len(result.cleared_namespaces),
488 "errors": len(result.errors),
489 "success": len(result.errors) == 0,
490 "operation_summary": {
491 "invalidated": result.invalidated_keys[:10],
492 "warmed": result.warmed_keys[:10],
493 "cleared": result.cleared_namespaces,
494 },
495 }