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

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

2

3 import typing as t

4 from types import ModuleType

5

6 from anyio import Path as AsyncPath

7

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

9 from .typed import TypedCache

10

11# Specialized type aliases for different cache types

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

13 TemplateRootCache = TypedCache[AsyncPath | None]

14 CompilationCache = TypedCache[str]

15 ModuleCache = TypedCache[ModuleType]

16

17

18 class 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

316 class 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