asset_cache.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. """
  2. Asset Caching System for Trixy Application
  3. This module provides a comprehensive caching system for assets to improve
  4. performance and reduce I/O operations in the Trixy asset management system.
  5. Features:
  6. - Memory-efficient LRU cache with configurable size limits
  7. - Compressed caching to reduce memory usage
  8. - Cache persistence across application restarts
  9. - Thread-safe operations for multi-satellite support
  10. - Cache warming and preloading capabilities
  11. - Detailed cache statistics and monitoring
  12. - Cache policies and eviction strategies
  13. - Integration with asset validation system
  14. Cache Types:
  15. - Memory cache: Fast in-memory storage for frequently accessed assets
  16. - Disk cache: Persistent storage for large or less frequently used assets
  17. - Compressed cache: Reduced memory footprint using compression
  18. - Metadata cache: Quick access to asset metadata without full loading
  19. Usage:
  20. from trixy_core.assets.asset_cache import AssetCache, CachePolicy
  21. # Create cache with policy
  22. policy = CachePolicy(max_entries=100, max_memory_bytes=50*1024*1024)
  23. cache = AssetCache(policy=policy)
  24. # Store and retrieve assets
  25. cache.put("audio/success.wav", asset_data, metadata)
  26. cached_data, metadata = cache.get("audio/success.wav")
  27. # Cache management
  28. cache.clear()
  29. stats = cache.get_stats()
  30. """
  31. import os
  32. import json
  33. import gzip
  34. import pickle
  35. import threading
  36. import time
  37. import weakref
  38. from pathlib import Path
  39. from typing import Dict, Any, Optional, List, Set, Union, Tuple, NamedTuple
  40. from dataclasses import dataclass, field
  41. from enum import Enum
  42. import hashlib
  43. from collections import OrderedDict
  44. import tempfile
  45. import shutil
  46. class CacheEntryStatus(Enum):
  47. """Status of a cache entry."""
  48. LOADING = "loading"
  49. CACHED = "cached"
  50. EXPIRED = "expired"
  51. INVALID = "invalid"
  52. EVICTED = "evicted"
  53. class CacheEvictionStrategy(Enum):
  54. """Cache eviction strategies."""
  55. LRU = "lru" # Least Recently Used
  56. LFU = "lfu" # Least Frequently Used
  57. FIFO = "fifo" # First In, First Out
  58. TTL = "ttl" # Time To Live based
  59. SIZE = "size" # Based on entry size
  60. @dataclass
  61. class CachePolicy:
  62. """Configuration policy for asset caching."""
  63. max_entries: int = 100 # Maximum number of cached entries
  64. max_memory_bytes: int = 100 * 1024 * 1024 # Maximum memory usage (100MB)
  65. max_entry_size: int = 10 * 1024 * 1024 # Maximum size per entry (10MB)
  66. default_ttl_seconds: float = 3600.0 # Default time-to-live (1 hour)
  67. eviction_strategy: CacheEvictionStrategy = CacheEvictionStrategy.LRU
  68. compress: bool = False # Whether to compress cached data
  69. compression_threshold: int = 1024 # Minimum size to compress (bytes)
  70. compression_level: int = 6 # Compression level (1-9, 6 is good balance)
  71. enable_persistence: bool = True # Enable disk persistence
  72. persistence_path: Optional[str] = None # Path for cache persistence
  73. enable_stats: bool = True # Enable statistics collection
  74. warmup_on_startup: bool = False # Warm cache on startup
  75. preload_patterns: List[str] = field(default_factory=list) # Patterns to preload
  76. class CacheEntry:
  77. """Represents a single cache entry."""
  78. def __init__(
  79. self,
  80. key: str,
  81. data: Any,
  82. metadata: Optional[Dict[str, Any]] = None,
  83. ttl_seconds: Optional[float] = None,
  84. compressed: bool = False
  85. ):
  86. self.key = key
  87. self.data = data
  88. self.metadata = metadata or {}
  89. self.compressed = compressed
  90. self.created_time = time.time()
  91. self.last_access_time = self.created_time
  92. self.access_count = 0
  93. self.size_bytes = self._calculate_size()
  94. self.status = CacheEntryStatus.CACHED
  95. # TTL handling
  96. if ttl_seconds is not None:
  97. self.expires_at = self.created_time + ttl_seconds
  98. else:
  99. self.expires_at = None
  100. # Compression
  101. if compressed and not self.compressed:
  102. self._compress_data()
  103. def _calculate_size(self) -> int:
  104. """Calculate the memory size of this cache entry."""
  105. try:
  106. # Estimate size using pickle serialization
  107. return len(pickle.dumps(self.data)) + len(pickle.dumps(self.metadata))
  108. except Exception:
  109. # Fallback estimation
  110. if hasattr(self.data, '__len__'):
  111. return len(self.data)
  112. return 1024 # Default estimate
  113. def _compress_data(self):
  114. """Compress the cached data."""
  115. try:
  116. if not self.compressed:
  117. compressed_data = gzip.compress(pickle.dumps(self.data))
  118. self.data = compressed_data
  119. self.compressed = True
  120. self.size_bytes = len(compressed_data) + len(pickle.dumps(self.metadata))
  121. except Exception as e:
  122. # Compression failed, keep original data
  123. pass
  124. def _decompress_data(self):
  125. """Decompress the cached data."""
  126. try:
  127. if self.compressed:
  128. decompressed_data = pickle.loads(gzip.decompress(self.data))
  129. return decompressed_data
  130. return self.data
  131. except Exception:
  132. # Decompression failed, return None to indicate error
  133. return None
  134. def access(self) -> Any:
  135. """Access the cached data, updating access statistics."""
  136. self.last_access_time = time.time()
  137. self.access_count += 1
  138. if self.compressed:
  139. return self._decompress_data()
  140. return self.data
  141. def is_expired(self) -> bool:
  142. """Check if this cache entry has expired."""
  143. if self.expires_at is None:
  144. return False
  145. return time.time() > self.expires_at
  146. def is_valid(self) -> bool:
  147. """Check if this cache entry is valid."""
  148. return self.status == CacheEntryStatus.CACHED and not self.is_expired()
  149. def update_metadata(self, metadata: Dict[str, Any]):
  150. """Update the metadata for this entry."""
  151. self.metadata.update(metadata)
  152. def to_dict(self) -> Dict[str, Any]:
  153. """Convert cache entry to dictionary for serialization."""
  154. return {
  155. 'key': self.key,
  156. 'metadata': self.metadata,
  157. 'compressed': self.compressed,
  158. 'created_time': self.created_time,
  159. 'last_access_time': self.last_access_time,
  160. 'access_count': self.access_count,
  161. 'size_bytes': self.size_bytes,
  162. 'expires_at': self.expires_at,
  163. 'status': self.status.value
  164. }
  165. @dataclass
  166. class CacheStats:
  167. """Cache statistics."""
  168. hits: int = 0
  169. misses: int = 0
  170. evictions: int = 0
  171. expirations: int = 0
  172. entries: int = 0
  173. memory_usage: int = 0
  174. compression_ratio: float = 0.0
  175. hit_rate: float = 0.0
  176. average_access_time: float = 0.0
  177. def update_hit_rate(self):
  178. """Update the hit rate calculation."""
  179. total = self.hits + self.misses
  180. if total > 0:
  181. self.hit_rate = self.hits / total
  182. else:
  183. self.hit_rate = 0.0
  184. def pprint(message: str) -> None:
  185. """
  186. Asset cache logging function.
  187. """
  188. print(f"[ASSET_CACHE] {message}")
  189. class AssetCache:
  190. """
  191. High-performance asset caching system with configurable policies.
  192. Provides memory-efficient caching with compression, persistence,
  193. and multiple eviction strategies for optimal performance.
  194. """
  195. def __init__(
  196. self,
  197. policy: Optional[CachePolicy] = None,
  198. name: str = "default"
  199. ):
  200. """
  201. Initialize the asset cache.
  202. Args:
  203. policy: Cache policy configuration
  204. name: Name identifier for this cache instance
  205. """
  206. self.policy = policy or CachePolicy()
  207. self.name = name
  208. # Cache storage
  209. self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
  210. self._lock = threading.RLock()
  211. # Statistics
  212. self._stats = CacheStats() if self.policy.enable_stats else None
  213. # Persistence
  214. self._persistence_path = None
  215. if self.policy.enable_persistence:
  216. self._setup_persistence()
  217. # Startup time
  218. self._startup_time = time.time()
  219. pprint(f"Asset cache '{name}' initialized with policy: "
  220. f"max_entries={policy.max_entries}, "
  221. f"max_memory={policy.max_memory_bytes//1024//1024}MB, "
  222. f"compression={policy.compress}")
  223. # Load persisted cache if available
  224. if self.policy.enable_persistence:
  225. self._load_persisted_cache()
  226. # Warm up cache if configured
  227. if self.policy.warmup_on_startup:
  228. self._warmup_cache()
  229. def _setup_persistence(self):
  230. """Setup cache persistence directory."""
  231. if self.policy.persistence_path:
  232. self._persistence_path = Path(self.policy.persistence_path)
  233. else:
  234. # Use temporary directory by default
  235. cache_dir = Path(tempfile.gettempdir()) / "trixy_asset_cache"
  236. self._persistence_path = cache_dir / self.name
  237. try:
  238. self._persistence_path.mkdir(parents=True, exist_ok=True)
  239. pprint(f"Cache persistence directory: {self._persistence_path}")
  240. except Exception as e:
  241. pprint(f"Warning: Could not create persistence directory: {e}")
  242. self._persistence_path = None
  243. def _load_persisted_cache(self):
  244. """Load cache entries from persistence."""
  245. if not self._persistence_path:
  246. return
  247. try:
  248. metadata_file = self._persistence_path / "cache_metadata.json"
  249. if metadata_file.exists():
  250. with open(metadata_file, 'r') as f:
  251. persisted_metadata = json.load(f)
  252. loaded_count = 0
  253. for entry_data in persisted_metadata.get('entries', []):
  254. try:
  255. key = entry_data['key']
  256. data_file = self._persistence_path / f"{self._key_to_filename(key)}.dat"
  257. if data_file.exists():
  258. with open(data_file, 'rb') as f:
  259. data = pickle.load(f)
  260. # Recreate cache entry
  261. entry = CacheEntry(
  262. key=key,
  263. data=data,
  264. metadata=entry_data.get('metadata', {}),
  265. compressed=entry_data.get('compressed', False)
  266. )
  267. # Restore entry properties
  268. entry.created_time = entry_data.get('created_time', time.time())
  269. entry.last_access_time = entry_data.get('last_access_time', time.time())
  270. entry.access_count = entry_data.get('access_count', 0)
  271. entry.size_bytes = entry_data.get('size_bytes', entry.size_bytes)
  272. entry.expires_at = entry_data.get('expires_at')
  273. # Check if still valid
  274. if not entry.is_expired():
  275. self._cache[key] = entry
  276. loaded_count += 1
  277. except Exception as entry_error:
  278. pprint(f"Warning: Could not load cache entry {key}: {entry_error}")
  279. pprint(f"Loaded {loaded_count} entries from cache persistence")
  280. except Exception as e:
  281. pprint(f"Warning: Could not load persisted cache: {e}")
  282. def _persist_cache(self):
  283. """Persist cache entries to disk."""
  284. if not self._persistence_path or not self.policy.enable_persistence:
  285. return
  286. try:
  287. # Save metadata
  288. metadata = {
  289. 'cache_name': self.name,
  290. 'created_time': self._startup_time,
  291. 'last_saved': time.time(),
  292. 'entries': []
  293. }
  294. # Save individual entries
  295. with self._lock:
  296. for key, entry in self._cache.items():
  297. if entry.is_valid():
  298. # Save entry data
  299. data_file = self._persistence_path / f"{self._key_to_filename(key)}.dat"
  300. with open(data_file, 'wb') as f:
  301. data_to_save = entry.data
  302. if entry.compressed:
  303. # Data is already compressed
  304. data_to_save = entry.data
  305. pickle.dump(data_to_save, f)
  306. # Add to metadata
  307. metadata['entries'].append(entry.to_dict())
  308. # Save metadata
  309. metadata_file = self._persistence_path / "cache_metadata.json"
  310. with open(metadata_file, 'w') as f:
  311. json.dump(metadata, f, indent=2)
  312. pprint(f"Persisted {len(metadata['entries'])} cache entries")
  313. except Exception as e:
  314. pprint(f"Warning: Could not persist cache: {e}")
  315. def _key_to_filename(self, key: str) -> str:
  316. """Convert cache key to safe filename."""
  317. # Create a hash of the key for safe filename
  318. return hashlib.md5(key.encode()).hexdigest()
  319. def _warmup_cache(self):
  320. """Warm up the cache with preload patterns."""
  321. if not self.policy.preload_patterns:
  322. return
  323. pprint(f"Warming up cache with {len(self.policy.preload_patterns)} patterns")
  324. # This would integrate with AssetManager to preload assets
  325. # For now, just log the intent
  326. def get(self, key: str) -> Tuple[Optional[Any], Optional[Dict[str, Any]]]:
  327. """
  328. Get an item from the cache.
  329. Args:
  330. key: Cache key
  331. Returns:
  332. Tuple of (data, metadata) if found, (None, None) if not found
  333. """
  334. with self._lock:
  335. entry = self._cache.get(key)
  336. if entry is None:
  337. self._record_miss()
  338. return None, None
  339. if not entry.is_valid():
  340. # Entry is expired or invalid
  341. self._remove_entry(key, reason="expired")
  342. self._record_miss()
  343. return None, None
  344. # Move to end for LRU
  345. if self.policy.eviction_strategy == CacheEvictionStrategy.LRU:
  346. self._cache.move_to_end(key)
  347. self._record_hit()
  348. data = entry.access()
  349. if data is None:
  350. # Decompression or access failed
  351. self._remove_entry(key, reason="corrupted")
  352. self._record_miss()
  353. return None, None
  354. return data, entry.metadata.copy()
  355. def put(
  356. self,
  357. key: str,
  358. data: Any,
  359. metadata: Optional[Dict[str, Any]] = None,
  360. ttl_seconds: Optional[float] = None
  361. ) -> bool:
  362. """
  363. Put an item into the cache.
  364. Args:
  365. key: Cache key
  366. data: Data to cache
  367. metadata: Optional metadata
  368. ttl_seconds: Time-to-live in seconds
  369. Returns:
  370. True if successfully cached, False otherwise
  371. """
  372. if not key or data is None:
  373. return False
  374. # Check if data is too large
  375. try:
  376. estimated_size = len(pickle.dumps(data))
  377. if estimated_size > self.policy.max_entry_size:
  378. pprint(f"Entry too large to cache: {key} ({estimated_size} bytes)")
  379. return False
  380. except Exception:
  381. # If we can't serialize it, we can't cache it
  382. return False
  383. with self._lock:
  384. # Determine if we should compress
  385. should_compress = (
  386. self.policy.compress and
  387. estimated_size >= self.policy.compression_threshold
  388. )
  389. # Use provided TTL or default
  390. ttl = ttl_seconds if ttl_seconds is not None else self.policy.default_ttl_seconds
  391. # Create cache entry
  392. entry = CacheEntry(
  393. key=key,
  394. data=data,
  395. metadata=metadata,
  396. ttl_seconds=ttl,
  397. compressed=should_compress
  398. )
  399. # Check if we need to make room
  400. self._ensure_capacity_for_entry(entry)
  401. # Add to cache
  402. if key in self._cache:
  403. # Update existing entry
  404. old_entry = self._cache[key]
  405. self._update_stats_for_removal(old_entry)
  406. self._cache[key] = entry
  407. # Move to end for LRU
  408. if self.policy.eviction_strategy == CacheEvictionStrategy.LRU:
  409. self._cache.move_to_end(key)
  410. self._update_stats_for_addition(entry)
  411. pprint(f"Cached asset: {key} ({entry.size_bytes} bytes, compressed: {entry.compressed})")
  412. return True
  413. def remove(self, key: str) -> bool:
  414. """
  415. Remove an item from the cache.
  416. Args:
  417. key: Cache key to remove
  418. Returns:
  419. True if item was removed, False if not found
  420. """
  421. with self._lock:
  422. return self._remove_entry(key, reason="manual")
  423. def _remove_entry(self, key: str, reason: str = "unknown") -> bool:
  424. """Remove an entry from the cache."""
  425. entry = self._cache.pop(key, None)
  426. if entry:
  427. self._update_stats_for_removal(entry)
  428. if reason == "expired":
  429. self._record_expiration()
  430. elif reason in ["eviction", "capacity"]:
  431. self._record_eviction()
  432. return True
  433. return False
  434. def clear(self) -> int:
  435. """
  436. Clear all items from the cache.
  437. Returns:
  438. Number of items removed
  439. """
  440. with self._lock:
  441. count = len(self._cache)
  442. self._cache.clear()
  443. if self._stats:
  444. self._stats.entries = 0
  445. self._stats.memory_usage = 0
  446. pprint(f"Cache cleared: {count} entries removed")
  447. return count
  448. def _ensure_capacity_for_entry(self, entry: CacheEntry):
  449. """Ensure there's capacity for a new entry."""
  450. # Check entry count limit
  451. while len(self._cache) >= self.policy.max_entries:
  452. self._evict_one_entry()
  453. # Check memory limit
  454. current_memory = self._calculate_memory_usage()
  455. while (current_memory + entry.size_bytes) > self.policy.max_memory_bytes:
  456. if not self._evict_one_entry():
  457. # Couldn't evict anything, can't add this entry
  458. break
  459. current_memory = self._calculate_memory_usage()
  460. def _evict_one_entry(self) -> bool:
  461. """Evict one entry based on the eviction strategy."""
  462. if not self._cache:
  463. return False
  464. key_to_evict = None
  465. if self.policy.eviction_strategy == CacheEvictionStrategy.LRU:
  466. # Remove least recently used (first item in OrderedDict)
  467. key_to_evict = next(iter(self._cache))
  468. elif self.policy.eviction_strategy == CacheEvictionStrategy.LFU:
  469. # Remove least frequently used
  470. min_access_count = float('inf')
  471. for key, entry in self._cache.items():
  472. if entry.access_count < min_access_count:
  473. min_access_count = entry.access_count
  474. key_to_evict = key
  475. elif self.policy.eviction_strategy == CacheEvictionStrategy.FIFO:
  476. # Remove oldest entry
  477. oldest_time = float('inf')
  478. for key, entry in self._cache.items():
  479. if entry.created_time < oldest_time:
  480. oldest_time = entry.created_time
  481. key_to_evict = key
  482. elif self.policy.eviction_strategy == CacheEvictionStrategy.TTL:
  483. # Remove entry with earliest expiration
  484. earliest_expiry = float('inf')
  485. for key, entry in self._cache.items():
  486. if entry.expires_at and entry.expires_at < earliest_expiry:
  487. earliest_expiry = entry.expires_at
  488. key_to_evict = key
  489. elif self.policy.eviction_strategy == CacheEvictionStrategy.SIZE:
  490. # Remove largest entry
  491. max_size = 0
  492. for key, entry in self._cache.items():
  493. if entry.size_bytes > max_size:
  494. max_size = entry.size_bytes
  495. key_to_evict = key
  496. if key_to_evict:
  497. self._remove_entry(key_to_evict, reason="eviction")
  498. return True
  499. return False
  500. def _calculate_memory_usage(self) -> int:
  501. """Calculate total memory usage of the cache."""
  502. return sum(entry.size_bytes for entry in self._cache.values())
  503. def cleanup_expired(self) -> int:
  504. """
  505. Remove expired entries from the cache.
  506. Returns:
  507. Number of expired entries removed
  508. """
  509. with self._lock:
  510. expired_keys = [
  511. key for key, entry in self._cache.items()
  512. if entry.is_expired()
  513. ]
  514. for key in expired_keys:
  515. self._remove_entry(key, reason="expired")
  516. if expired_keys:
  517. pprint(f"Cleaned up {len(expired_keys)} expired cache entries")
  518. return len(expired_keys)
  519. def get_stats(self) -> Dict[str, Any]:
  520. """
  521. Get cache statistics.
  522. Returns:
  523. Dictionary containing cache statistics
  524. """
  525. if not self._stats:
  526. return {}
  527. with self._lock:
  528. self._stats.entries = len(self._cache)
  529. self._stats.memory_usage = self._calculate_memory_usage()
  530. self._stats.update_hit_rate()
  531. # Calculate compression ratio
  532. if self.policy.compress:
  533. total_compressed = sum(
  534. entry.size_bytes for entry in self._cache.values()
  535. if entry.compressed
  536. )
  537. total_uncompressed = sum(
  538. len(pickle.dumps(entry.data)) for entry in self._cache.values()
  539. if not entry.compressed
  540. )
  541. if total_uncompressed > 0:
  542. self._stats.compression_ratio = total_compressed / total_uncompressed
  543. return {
  544. 'hits': self._stats.hits,
  545. 'misses': self._stats.misses,
  546. 'hit_rate': self._stats.hit_rate,
  547. 'evictions': self._stats.evictions,
  548. 'expirations': self._stats.expirations,
  549. 'entries': self._stats.entries,
  550. 'memory_usage_bytes': self._stats.memory_usage,
  551. 'memory_usage_mb': self._stats.memory_usage / (1024 * 1024),
  552. 'compression_ratio': self._stats.compression_ratio,
  553. 'uptime_seconds': time.time() - self._startup_time,
  554. 'policy': {
  555. 'max_entries': self.policy.max_entries,
  556. 'max_memory_mb': self.policy.max_memory_bytes / (1024 * 1024),
  557. 'eviction_strategy': self.policy.eviction_strategy.value,
  558. 'compression_enabled': self.policy.compress,
  559. }
  560. }
  561. def get_entry_info(self, key: str) -> Optional[Dict[str, Any]]:
  562. """
  563. Get information about a specific cache entry.
  564. Args:
  565. key: Cache key
  566. Returns:
  567. Dictionary with entry information or None if not found
  568. """
  569. with self._lock:
  570. entry = self._cache.get(key)
  571. if entry:
  572. return {
  573. 'key': entry.key,
  574. 'size_bytes': entry.size_bytes,
  575. 'compressed': entry.compressed,
  576. 'created_time': entry.created_time,
  577. 'last_access_time': entry.last_access_time,
  578. 'access_count': entry.access_count,
  579. 'expires_at': entry.expires_at,
  580. 'is_expired': entry.is_expired(),
  581. 'is_valid': entry.is_valid(),
  582. 'status': entry.status.value,
  583. 'metadata': entry.metadata.copy()
  584. }
  585. return None
  586. def list_keys(self) -> List[str]:
  587. """
  588. Get a list of all cache keys.
  589. Returns:
  590. List of cache keys
  591. """
  592. with self._lock:
  593. return list(self._cache.keys())
  594. def _record_hit(self):
  595. """Record a cache hit."""
  596. if self._stats:
  597. self._stats.hits += 1
  598. def _record_miss(self):
  599. """Record a cache miss."""
  600. if self._stats:
  601. self._stats.misses += 1
  602. def _record_eviction(self):
  603. """Record a cache eviction."""
  604. if self._stats:
  605. self._stats.evictions += 1
  606. def _record_expiration(self):
  607. """Record a cache expiration."""
  608. if self._stats:
  609. self._stats.expirations += 1
  610. def _update_stats_for_addition(self, entry: CacheEntry):
  611. """Update statistics when adding an entry."""
  612. # Stats are updated in real-time through other methods
  613. pass
  614. def _update_stats_for_removal(self, entry: CacheEntry):
  615. """Update statistics when removing an entry."""
  616. # Stats are updated in real-time through other methods
  617. pass
  618. def shutdown(self):
  619. """Shutdown the cache and save persistence."""
  620. pprint(f"Shutting down asset cache '{self.name}'...")
  621. # Persist cache if enabled
  622. if self.policy.enable_persistence:
  623. self._persist_cache()
  624. # Clear cache
  625. with self._lock:
  626. self._cache.clear()
  627. pprint(f"Asset cache '{self.name}' shutdown complete")
  628. def __enter__(self):
  629. """Context manager entry."""
  630. return self
  631. def __exit__(self, exc_type, exc_val, exc_tb):
  632. """Context manager exit."""
  633. self.shutdown()
  634. # Convenience functions
  635. def create_memory_cache(
  636. max_entries: int = 100,
  637. max_memory_mb: int = 100,
  638. enable_compression: bool = False
  639. ) -> AssetCache:
  640. """
  641. Create a simple memory-only cache.
  642. Args:
  643. max_entries: Maximum number of entries
  644. max_memory_mb: Maximum memory usage in MB
  645. enable_compression: Whether to enable compression
  646. Returns:
  647. AssetCache instance
  648. """
  649. policy = CachePolicy(
  650. max_entries=max_entries,
  651. max_memory_bytes=max_memory_mb * 1024 * 1024,
  652. compress=enable_compression,
  653. enable_persistence=False
  654. )
  655. return AssetCache(policy=policy, name="memory")
  656. def create_persistent_cache(
  657. cache_dir: str,
  658. max_entries: int = 500,
  659. max_memory_mb: int = 200,
  660. enable_compression: bool = True
  661. ) -> AssetCache:
  662. """
  663. Create a cache with disk persistence.
  664. Args:
  665. cache_dir: Directory for cache persistence
  666. max_entries: Maximum number of entries
  667. max_memory_mb: Maximum memory usage in MB
  668. enable_compression: Whether to enable compression
  669. Returns:
  670. AssetCache instance
  671. """
  672. policy = CachePolicy(
  673. max_entries=max_entries,
  674. max_memory_bytes=max_memory_mb * 1024 * 1024,
  675. compress=enable_compression,
  676. enable_persistence=True,
  677. persistence_path=cache_dir,
  678. warmup_on_startup=True
  679. )
  680. return AssetCache(policy=policy, name="persistent")
  681. # Module exports
  682. __all__ = [
  683. 'AssetCache',
  684. 'CachePolicy',
  685. 'CacheEntry',
  686. 'CacheEntryStatus',
  687. 'CacheEvictionStrategy',
  688. 'CacheStats',
  689. 'create_memory_cache',
  690. 'create_persistent_cache',
  691. 'pprint'
  692. ]