Coverage for jinja2_async_environment/caching/manager.py: 33%

136 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-03 14:09 -0700

1"""Cache manager for dependency injection and centralized cache control.""" 

2 

3import typing as t 

4from types import ModuleType 

5 

6from anyio import Path as AsyncPath 

7 

8from .strategies import AdaptiveCache, CacheWarmer, HierarchicalCache, LFUCache 

9from .typed import TypedCache 

10 

11# Specialized type aliases for different cache types 

12PackageSpecCache = TypedCache[tuple[t.Any, t.Any]] 

13TemplateRootCache = TypedCache[AsyncPath | None] 

14CompilationCache = TypedCache[str] 

15ModuleCache = TypedCache[ModuleType] 

16 

17 

18class CacheManager: 

19 """Centralized cache management with dependency injection support. 

20 

21 This manager provides type-safe caches for different use cases while 

22 allowing dependency injection and proper resource management. 

23 """ 

24 

25 def __init__( 

26 self, 

27 package_cache_size: int = 500, 

28 template_cache_size: int = 1000, 

29 compilation_cache_size: int = 2000, 

30 module_cache_size: int = 200, 

31 default_ttl: int = 300, 

32 ): 

33 """Initialize the cache manager. 

34 

35 Args: 

36 package_cache_size: Maximum size for package spec cache 

37 template_cache_size: Maximum size for template root cache 

38 compilation_cache_size: Maximum size for compilation cache 

39 module_cache_size: Maximum size for module import cache 

40 default_ttl: Default TTL for all caches in seconds 

41 """ 

42 # Initialize type-safe caches 

43 self.package_cache: PackageSpecCache = TypedCache( 

44 max_size=package_cache_size, 

45 default_ttl=default_ttl * 6, # Package specs change rarely 

46 ) 

47 

48 self.template_cache: TemplateRootCache = TypedCache( 

49 max_size=template_cache_size, 

50 default_ttl=default_ttl * 6, # Template roots change rarely 

51 ) 

52 

53 self.compilation_cache: CompilationCache = TypedCache( 

54 max_size=compilation_cache_size, 

55 default_ttl=default_ttl * 2, # Compiled templates may change 

56 ) 

57 

58 self.module_cache: ModuleCache = TypedCache( 

59 max_size=module_cache_size, 

60 default_ttl=default_ttl * 12, # Modules change very rarely 

61 ) 

62 

63 self._default_ttl = default_ttl 

64 

65 def clear_all(self) -> None: 

66 """Clear all caches.""" 

67 self.package_cache.clear() 

68 self.template_cache.clear() 

69 self.compilation_cache.clear() 

70 self.module_cache.clear() 

71 

72 def cleanup_expired(self) -> dict[str, int]: 

73 """Clean up expired entries from all caches. 

74 

75 Returns: 

76 Dictionary with count of expired entries per cache 

77 """ 

78 return { 

79 "package_cache": self.package_cache.cleanup_expired(), 

80 "template_cache": self.template_cache.cleanup_expired(), 

81 "compilation_cache": self.compilation_cache.cleanup_expired(), 

82 "module_cache": self.module_cache.cleanup_expired(), 

83 } 

84 

85 def get_statistics(self) -> dict[str, dict[str, t.Any]]: 

86 """Get statistics for all caches. 

87 

88 Returns: 

89 Dictionary with statistics for each cache 

90 """ 

91 return { 

92 "package_cache": self.package_cache.get_statistics(), 

93 "template_cache": self.template_cache.get_statistics(), 

94 "compilation_cache": self.compilation_cache.get_statistics(), 

95 "module_cache": self.module_cache.get_statistics(), 

96 } 

97 

98 def resize_caches( 

99 self, 

100 package_size: int | None = None, 

101 template_size: int | None = None, 

102 compilation_size: int | None = None, 

103 module_size: int | None = None, 

104 ) -> None: 

105 """Resize cache maximum sizes. 

106 

107 Args: 

108 package_size: New size for package cache (None to keep current) 

109 template_size: New size for template cache (None to keep current) 

110 compilation_size: New size for compilation cache (None to keep current) 

111 module_size: New size for module cache (None to keep current) 

112 """ 

113 if package_size is not None: 

114 self.package_cache.resize(package_size) 

115 if template_size is not None: 

116 self.template_cache.resize(template_size) 

117 if compilation_size is not None: 

118 self.compilation_cache.resize(compilation_size) 

119 if module_size is not None: 

120 self.module_cache.resize(module_size) 

121 

122 @classmethod 

123 def get_default(cls) -> "CacheManager": 

124 """Get the default global cache manager instance. 

125 

126 This provides backward compatibility while allowing dependency injection. 

127 

128 Returns: 

129 Default cache manager instance 

130 """ 

131 if not hasattr(cls, "_default_instance"): 

132 cls._default_instance = cls() 

133 return cls._default_instance 

134 

135 @classmethod 

136 def set_default(cls, manager: "CacheManager") -> None: 

137 """Set a new default cache manager instance. 

138 

139 Args: 

140 manager: New cache manager to use as default 

141 """ 

142 cls._default_instance = manager 

143 

144 def create_scoped_manager( 

145 self, 

146 package_ttl: int | None = None, 

147 template_ttl: int | None = None, 

148 compilation_ttl: int | None = None, 

149 module_ttl: int | None = None, 

150 ) -> "CacheManager": 

151 """Create a new cache manager with different TTL settings. 

152 

153 Useful for creating isolated cache environments for testing 

154 or different application contexts. 

155 

156 Args: 

157 package_ttl: TTL for package cache (None to use default) 

158 template_ttl: TTL for template cache (None to use default) 

159 compilation_ttl: TTL for compilation cache (None to use default) 

160 module_ttl: TTL for module cache (None to use default) 

161 

162 Returns: 

163 New cache manager with specified TTL settings 

164 """ 

165 # Use current cache sizes for the scoped manager 

166 stats = self.get_statistics() 

167 

168 manager = CacheManager( 

169 package_cache_size=stats["package_cache"]["max_size"], 

170 template_cache_size=stats["template_cache"]["max_size"], 

171 compilation_cache_size=stats["compilation_cache"]["max_size"], 

172 module_cache_size=stats["module_cache"]["max_size"], 

173 default_ttl=self._default_ttl, 

174 ) 

175 

176 # Override specific TTLs if provided 

177 if package_ttl is not None: 

178 manager.package_cache._default_ttl = package_ttl 

179 if template_ttl is not None: 

180 manager.template_cache._default_ttl = template_ttl 

181 if compilation_ttl is not None: 

182 manager.compilation_cache._default_ttl = compilation_ttl 

183 if module_ttl is not None: 

184 manager.module_cache._default_ttl = module_ttl 

185 

186 return manager 

187 

188 def get_memory_usage_estimate(self) -> dict[str, int]: 

189 """Get rough memory usage estimates for all caches. 

190 

191 Returns: 

192 Dictionary with estimated memory usage per cache in bytes 

193 """ 

194 # Rough estimates based on typical object sizes 

195 AVERAGE_SIZES = { 

196 "package_spec": 200, # Loader + spec objects 

197 "template_root": 100, # Path objects 

198 "compilation": 5000, # Compiled template code 

199 "module": 1000, # Module objects 

200 } 

201 

202 stats = self.get_statistics() 

203 

204 memory_usage = {} 

205 for cache_name in [ 

206 "package_cache", 

207 "template_cache", 

208 "compilation_cache", 

209 "module_cache", 

210 ]: 

211 cache_stats = stats.get(cache_name, {}) 

212 if isinstance(cache_stats, dict) and "size" in cache_stats: 

213 size = cache_stats["size"] 

214 else: 

215 # Handle hierarchical cache which might not have direct size 

216 size = getattr(getattr(self, cache_name, None), "__len__", lambda: 0)() 

217 if callable(size): 

218 size = size() 

219 

220 avg_size = AVERAGE_SIZES.get(cache_name.replace("_cache", ""), 100) 

221 memory_usage[cache_name] = size * avg_size 

222 

223 return memory_usage 

224 

225 def get(self, cache_type: str, key: str) -> t.Any: 

226 """Get value from specified cache. 

227 

228 Args: 

229 cache_type: Type of cache ('package', 'template', 'compilation', 'module') 

230 key: Cache key 

231 

232 Returns: 

233 Cached value or None if not found 

234 

235 Raises: 

236 ValueError: If cache_type is not recognized 

237 """ 

238 cache_map = { 

239 "package": self.package_cache, 

240 "template": self.template_cache, 

241 "compilation": self.compilation_cache, 

242 "module": self.module_cache, 

243 } 

244 

245 if cache_type not in cache_map: 

246 raise ValueError(f"Unknown cache type: {cache_type}") 

247 

248 return cache_map[cache_type].get(key) 

249 

250 def set( 

251 self, cache_type: str, key: str, value: t.Any, ttl: int | None = None 

252 ) -> None: 

253 """Set value in specified cache. 

254 

255 Args: 

256 cache_type: Type of cache ('package', 'template', 'compilation', 'module') 

257 key: Cache key 

258 value: Value to store 

259 ttl: Time-to-live in seconds (uses cache default if None) 

260 

261 Raises: 

262 ValueError: If cache_type is not recognized 

263 """ 

264 cache_map = { 

265 "package": self.package_cache, 

266 "template": self.template_cache, 

267 "compilation": self.compilation_cache, 

268 "module": self.module_cache, 

269 } 

270 

271 if cache_type not in cache_map: 

272 raise ValueError(f"Unknown cache type: {cache_type}") 

273 

274 cache_map[cache_type].set(key, value, ttl) 

275 

276 def delete(self, cache_type: str, key: str) -> bool: 

277 """Delete value from specified cache. 

278 

279 Args: 

280 cache_type: Type of cache ('package', 'template', 'compilation', 'module') 

281 key: Cache key 

282 

283 Returns: 

284 True if key was deleted, False if not found 

285 

286 Raises: 

287 ValueError: If cache_type is not recognized 

288 """ 

289 cache_map = { 

290 "package": self.package_cache, 

291 "template": self.template_cache, 

292 "compilation": self.compilation_cache, 

293 "module": self.module_cache, 

294 } 

295 

296 if cache_type not in cache_map: 

297 raise ValueError(f"Unknown cache type: {cache_type}") 

298 

299 return cache_map[cache_type].delete(key) 

300 

301 def create_cache_warmer(self) -> CacheWarmer: 

302 """Create a cache warmer for this cache manager. 

303 

304 Returns: 

305 CacheWarmer instance for preloading caches 

306 """ 

307 return CacheWarmer(self) 

308 

309 def __repr__(self) -> str: 

310 """String representation of cache manager.""" 

311 stats = self.get_statistics() 

312 total_size = sum(cache["size"] for cache in stats.values()) 

313 return f"CacheManager(total_entries={total_size}, caches=4)" 

314 

315 

316class AdvancedCacheManager(CacheManager): 

317 """Enhanced cache manager with advanced strategies and monitoring.""" 

318 

319 def __init__( 

320 self, 

321 strategy: str = "adaptive", # "lru", "lfu", "adaptive", "hierarchical" 

322 package_cache_size: int = 500, 

323 template_cache_size: int = 1000, 

324 compilation_cache_size: int = 2000, 

325 module_cache_size: int = 200, 

326 default_ttl: int = 300, 

327 enable_hierarchical: bool = False, 

328 l1_cache_size: int = 100, 

329 ): 

330 """Initialize advanced cache manager. 

331 

332 Args: 

333 strategy: Cache strategy to use ("lru", "lfu", "adaptive", "hierarchical") 

334 package_cache_size: Size for package cache 

335 template_cache_size: Size for template cache 

336 compilation_cache_size: Size for compilation cache 

337 module_cache_size: Size for module cache 

338 default_ttl: Default TTL in seconds 

339 enable_hierarchical: Enable hierarchical caching for templates 

340 l1_cache_size: L1 cache size for hierarchical mode 

341 """ 

342 # Don't call super().__init__() - we'll create our own caches 

343 self._strategy = strategy 

344 self._enable_hierarchical = enable_hierarchical 

345 self._default_ttl = default_ttl 

346 

347 # Create caches based on strategy 

348 self._create_caches( 

349 strategy, 

350 package_cache_size, 

351 template_cache_size, 

352 compilation_cache_size, 

353 module_cache_size, 

354 default_ttl, 

355 l1_cache_size, 

356 ) 

357 

358 def _create_caches( 

359 self, 

360 strategy: str, 

361 package_size: int, 

362 template_size: int, 

363 compilation_size: int, 

364 module_size: int, 

365 ttl: int, 

366 l1_size: int, 

367 ) -> None: 

368 """Create caches with specified strategy.""" 

369 cache_factory = { 

370 "lru": lambda size, ttl: TypedCache(max_size=size, default_ttl=ttl), 

371 "lfu": lambda size, ttl: LFUCache(max_size=size, default_ttl=ttl), 

372 "adaptive": lambda size, ttl: AdaptiveCache(max_size=size, default_ttl=ttl), 

373 } 

374 

375 factory = cache_factory.get(strategy, cache_factory["lru"]) 

376 

377 # Create standard caches 

378 self.package_cache = factory(package_size, ttl * 6) 

379 self.compilation_cache = factory(compilation_size, ttl * 2) 

380 self.module_cache = factory(module_size, ttl * 12) 

381 

382 # Create template cache (potentially hierarchical) 

383 if self._enable_hierarchical: 

384 self.template_cache = HierarchicalCache( 

385 l1_size=l1_size, l2_size=template_size, l1_ttl=ttl, l2_ttl=ttl * 6 

386 ) 

387 else: 

388 self.template_cache = factory(template_size, ttl * 6) 

389 

390 def get_extended_statistics(self) -> dict[str, t.Any]: 

391 """Get extended statistics for all caches.""" 

392 stats = self.get_statistics() 

393 

394 # Add strategy-specific statistics 

395 extended_stats = { 

396 "strategy": self._strategy, 

397 "hierarchical_enabled": self._enable_hierarchical, 

398 "base_stats": stats, 

399 } 

400 

401 # Get advanced statistics from caches that support it 

402 for cache_name, cache in [ 

403 ("package_cache", self.package_cache), 

404 ("template_cache", self.template_cache), 

405 ("compilation_cache", self.compilation_cache), 

406 ("module_cache", self.module_cache), 

407 ]: 

408 if hasattr(cache, "get_extended_statistics"): 

409 extended_stats[f"{cache_name}_extended"] = ( 

410 cache.get_extended_statistics() 

411 ) 

412 elif hasattr(cache, "get_strategy_info"): 

413 extended_stats[f"{cache_name}_strategy"] = cache.get_strategy_info() 

414 

415 return extended_stats 

416 

417 def optimize_caches(self) -> dict[str, t.Any]: 

418 """Perform cache optimization and return results. 

419 

420 Returns: 

421 Dictionary with optimization results 

422 """ 

423 results = {} 

424 

425 # Cleanup expired entries 

426 cleanup_results = self.cleanup_expired() 

427 results["cleanup"] = cleanup_results 

428 

429 # Trigger strategy evaluation for adaptive caches 

430 for cache_name, cache in [ 

431 ("package_cache", self.package_cache), 

432 ("template_cache", self.template_cache), 

433 ("compilation_cache", self.compilation_cache), 

434 ("module_cache", self.module_cache), 

435 ]: 

436 if hasattr(cache, "_evaluate_strategy"): 

437 old_strategy = getattr(cache, "_strategy", "unknown") 

438 cache._evaluate_strategy() 

439 new_strategy = getattr(cache, "_strategy", "unknown") 

440 results[f"{cache_name}_strategy_change"] = { 

441 "old": old_strategy, 

442 "new": new_strategy, 

443 "changed": old_strategy != new_strategy, 

444 } 

445 

446 return results 

447 

448 def get_memory_efficiency_report(self) -> dict[str, t.Any]: 

449 """Generate detailed memory efficiency report.""" 

450 stats = self.get_extended_statistics() 

451 memory_usage = self.get_memory_usage_estimate() 

452 

453 total_memory = sum(memory_usage.values()) 

454 total_entries = sum(cache["size"] for cache in stats["base_stats"].values()) 

455 

456 # Calculate efficiency metrics 

457 efficiency_report = { 

458 "total_memory_bytes": total_memory, 

459 "total_entries": total_entries, 

460 "avg_memory_per_entry": total_memory / total_entries 

461 if total_entries > 0 

462 else 0, 

463 "memory_by_cache": memory_usage, 

464 "cache_utilization": {}, 

465 "recommendations": [], 

466 } 

467 

468 # Calculate utilization per cache 

469 for cache_name, cache_stats in stats["base_stats"].items(): 

470 utilization = cache_stats["fill_ratio"] 

471 efficiency_report["cache_utilization"][cache_name] = { 

472 "fill_ratio": utilization, 

473 "hit_rate": cache_stats["hit_rate"], 

474 "efficiency_score": utilization * cache_stats["hit_rate"], 

475 } 

476 

477 # Generate recommendations 

478 if utilization < 0.3 and cache_stats["size"] > 0: 

479 efficiency_report["recommendations"].append( 

480 f"Consider reducing {cache_name} size (low utilization: {utilization:.1%})" 

481 ) 

482 elif utilization > 0.9: 

483 efficiency_report["recommendations"].append( 

484 f"Consider increasing {cache_name} size (high utilization: {utilization:.1%})" 

485 ) 

486 

487 if cache_stats["hit_rate"] < 0.5 and cache_stats["size"] > 0: 

488 efficiency_report["recommendations"].append( 

489 f"Consider tuning {cache_name} TTL (low hit rate: {cache_stats['hit_rate']:.1%})" 

490 ) 

491 

492 return efficiency_report