Coverage for fastblocks/actions/sync/cache.py: 28%
272 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -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 (
271 gather_middleware,
272 gather_routes,
273 gather_templates,
274 )
276 try:
277 routes_result = await gather_routes()
278 if routes_result.total_routes > 0:
279 result.warmed_keys.append("gather:routes")
280 debug("Warmed gather routes cache")
281 except Exception as e:
282 debug(f"Error warming routes cache: {e}")
284 try:
285 templates_result = await gather_templates()
286 if templates_result.total_components > 0:
287 result.warmed_keys.append("gather:templates")
288 debug("Warmed gather templates cache")
289 except Exception as e:
290 debug(f"Error warming templates cache: {e}")
292 try:
293 middleware_result = await gather_middleware()
294 if middleware_result.total_middleware > 0:
295 result.warmed_keys.append("gather:middleware")
296 debug("Warmed gather middleware cache")
297 except Exception as e:
298 debug(f"Error warming middleware cache: {e}")
300 except Exception as e:
301 debug(f"Error warming gather cache: {e}")
304async def invalidate_template_cache(
305 template_paths: list[str] | None = None,
306 cache_namespace: str = "templates",
307) -> dict[str, t.Any]:
308 result: dict[str, t.Any] = {
309 "invalidated": [],
310 "errors": [],
311 }
313 try:
314 from acb.depends import depends
316 cache = depends.get("cache")
318 if not cache:
319 result["errors"].append("Cache adapter not available")
320 return result
322 if not template_paths:
323 pattern = f"{cache_namespace}:*"
324 deleted_keys = await cache.delete_pattern(pattern)
325 result["invalidated"].extend(deleted_keys or [pattern])
326 else:
327 for template_path in template_paths:
328 try:
329 template_key = f"{cache_namespace}:{template_path}"
330 await cache.delete(template_key)
331 result["invalidated"].append(template_key)
333 bytecode_key = f"bccache:{template_path}"
334 await cache.delete(bytecode_key)
335 result["invalidated"].append(bytecode_key)
337 await cache.delete_pattern(f"{cache_namespace}:*:{template_path}")
338 await cache.delete_pattern(f"bccache:*:{template_path}")
340 debug(f"Invalidated cache for template: {template_path}")
342 except Exception as e:
343 result["errors"].append(f"{template_path}: {e}")
345 except Exception as e:
346 result["errors"].append(str(e))
347 debug(f"Error invalidating template cache: {e}")
349 return result
352async def get_cache_stats(
353 namespaces: list[str] | None = None,
354) -> dict[str, t.Any]:
355 if namespaces is None:
356 namespaces = ["templates", "bccache", "responses", "gather"]
358 stats: dict[str, t.Any] = {
359 "total_keys": 0,
360 "namespaces": {},
361 "memory_usage": 0,
362 "hit_rate": 0.0,
363 "errors": [],
364 }
366 try:
367 cache = await _get_cache_adapter(stats)
368 if not cache:
369 return stats
371 await _collect_cache_info(cache, stats)
372 await _collect_namespace_stats(cache, namespaces, stats)
374 except Exception as e:
375 stats["errors"].append(str(e))
376 debug(f"Error getting cache stats: {e}")
378 return stats
381async def _get_cache_adapter(stats: dict[str, t.Any]) -> t.Any:
382 from acb.depends import depends
384 cache = depends.get("cache")
385 if not cache:
386 stats["errors"].append("Cache adapter not available")
387 return cache
390async def _collect_cache_info(cache: t.Any, stats: dict[str, t.Any]) -> None:
391 try:
392 info = await cache.info()
393 stats["memory_usage"] = info.get("used_memory", 0)
394 hits = info.get("keyspace_hits", 0)
395 misses = info.get("keyspace_misses", 0)
396 stats["hit_rate"] = hits / max(hits + misses, 1)
397 except Exception as e:
398 stats["errors"].append(f"Error getting cache info: {e}")
401async def _collect_namespace_stats(
402 cache: t.Any,
403 namespaces: list[str],
404 stats: dict[str, t.Any],
405) -> None:
406 for namespace in namespaces:
407 try:
408 pattern = f"{namespace}:*"
409 keys = await cache.keys(pattern)
410 key_count = len(keys) if keys else 0
412 stats["namespaces"][namespace] = {
413 "key_count": key_count,
414 "sample_keys": keys[:5] if keys else [],
415 }
417 stats["total_keys"] += key_count
419 except Exception as e:
420 stats["errors"].append(f"Error checking namespace {namespace}: {e}")
423async def optimize_cache(
424 max_memory_mb: int = 512,
425 eviction_policy: str = "allkeys-lru",
426) -> dict[str, t.Any]:
427 result: dict[str, t.Any] = {
428 "optimizations": [],
429 "warnings": [],
430 "errors": [],
431 }
433 try:
434 from acb.depends import depends
436 cache = depends.get("cache")
438 if not cache:
439 result["errors"].append("Cache adapter not available")
440 return result
442 try:
443 max_memory_bytes = max_memory_mb * 1024 * 1024
444 await cache.config_set("maxmemory", max_memory_bytes)
445 result["optimizations"].append(f"Set max memory to {max_memory_mb}MB")
446 except Exception as e:
447 result["errors"].append(f"Error setting max memory: {e}")
449 try:
450 await cache.config_set("maxmemory-policy", eviction_policy)
451 result["optimizations"].append(f"Set eviction policy to {eviction_policy}")
452 except Exception as e:
453 result["errors"].append(f"Error setting eviction policy: {e}")
455 stats = await get_cache_stats()
457 if stats["hit_rate"] < 0.8:
458 result["warnings"].append(f"Low cache hit rate: {stats['hit_rate']:.2%}")
460 if stats["total_keys"] > 10000:
461 result["warnings"].append(f"High key count: {stats['total_keys']}")
463 for namespace, info in stats["namespaces"].items():
464 if info["key_count"] > 5000:
465 result["warnings"].append(
466 f"Namespace {namespace} has {info['key_count']} keys",
467 )
469 except Exception as e:
470 result["errors"].append(str(e))
471 debug(f"Error optimizing cache: {e}")
473 return result
476def get_cache_sync_summary(result: CacheSyncResult) -> dict[str, t.Any]:
477 return {
478 "invalidated_count": len(result.invalidated_keys),
479 "warmed_count": len(result.warmed_keys),
480 "cleared_namespaces": len(result.cleared_namespaces),
481 "errors": len(result.errors),
482 "success": len(result.errors) == 0,
483 "operation_summary": {
484 "invalidated": result.invalidated_keys[:10],
485 "warmed": result.warmed_keys[:10],
486 "cleared": result.cleared_namespaces,
487 },
488 }