Coverage for mcpgateway/cache/resource_cache.py: 47%
56 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
1# -*- coding: utf-8 -*-
2"""Resource Cache Implementation.
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
8This module implements a simple in-memory cache with TTL expiration for caching
9resource content in the MCP Gateway. Features:
10- TTL-based expiration
11- Maximum size limit with LRU eviction
12- Thread-safe operations
13"""
15import asyncio
16import logging
17import time
18from dataclasses import dataclass
19from typing import Any, Dict, Optional
21logger = logging.getLogger(__name__)
24@dataclass
25class CacheEntry:
26 """Cache entry with expiration."""
28 value: Any
29 expires_at: float
30 last_access: float
33class ResourceCache:
34 """Resource content cache with TTL expiration.
36 Attributes:
37 max_size: Maximum number of entries
38 ttl: Time-to-live in seconds
39 _cache: Cache storage
40 _lock: Async lock for thread safety
41 """
43 def __init__(self, max_size: int = 1000, ttl: int = 3600):
44 """Initialize cache.
46 Args:
47 max_size: Maximum number of entries
48 ttl: Time-to-live in seconds
49 """
50 self.max_size = max_size
51 self.ttl = ttl
52 self._cache: Dict[str, CacheEntry] = {}
53 self._lock = asyncio.Lock()
55 async def initialize(self) -> None:
56 """Initialize cache service."""
57 logger.info("Initializing resource cache")
58 # Start cleanup task
59 asyncio.create_task(self._cleanup_loop())
61 async def shutdown(self) -> None:
62 """Shutdown cache service."""
63 logger.info("Shutting down resource cache")
64 self.clear()
66 def get(self, key: str) -> Optional[Any]:
67 """Get value from cache.
69 Args:
70 key: Cache key
72 Returns:
73 Cached value or None if not found/expired
74 """
75 if key not in self._cache: 75 ↛ 78line 75 didn't jump to line 78 because the condition on line 75 was always true
76 return None
78 entry = self._cache[key]
79 now = time.time()
81 # Check expiration
82 if now > entry.expires_at:
83 del self._cache[key]
84 return None
86 # Update access time
87 entry.last_access = now
88 return entry.value
90 def set(self, key: str, value: Any) -> None:
91 """Set value in cache.
93 Args:
94 key: Cache key
95 value: Value to cache
96 """
97 now = time.time()
99 # Check size limit
100 if len(self._cache) >= self.max_size: 100 ↛ 102line 100 didn't jump to line 102 because the condition on line 100 was never true
101 # Remove least recently used
102 lru_key = min(self._cache.keys(), key=lambda k: self._cache[k].last_access)
103 del self._cache[lru_key]
105 # Add new entry
106 self._cache[key] = CacheEntry(value=value, expires_at=now + self.ttl, last_access=now)
108 def delete(self, key: str) -> None:
109 """Delete value from cache.
111 Args:
112 key: Cache key to delete
113 """
114 self._cache.pop(key, None)
116 def clear(self) -> None:
117 """Clear all cached entries."""
118 self._cache.clear()
120 async def _cleanup_loop(self) -> None:
121 """Background task to clean expired entries."""
122 while True:
123 try:
124 async with self._lock:
125 now = time.time()
126 expired = [key for key, entry in self._cache.items() if now > entry.expires_at]
127 for key in expired:
128 del self._cache[key]
130 if expired:
131 logger.debug(f"Cleaned {len(expired)} expired cache entries")
133 except Exception as e:
134 logger.error(f"Cache cleanup error: {e}")
136 await asyncio.sleep(60) # Run every minute