Coverage for mcpgateway/cache/resource_cache.py: 91%
56 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +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"""
15# Standard
16import asyncio
17from dataclasses import dataclass
18import logging
19import time
20from typing import Any, Dict, Optional
22logger = logging.getLogger(__name__)
25@dataclass
26class CacheEntry:
27 """Cache entry with expiration."""
29 value: Any
30 expires_at: float
31 last_access: float
34class ResourceCache:
35 """Resource content cache with TTL expiration.
37 Attributes:
38 max_size: Maximum number of entries
39 ttl: Time-to-live in seconds
40 _cache: Cache storage
41 _lock: Async lock for thread safety
42 """
44 def __init__(self, max_size: int = 1000, ttl: int = 3600):
45 """Initialize cache.
47 Args:
48 max_size: Maximum number of entries
49 ttl: Time-to-live in seconds
50 """
51 self.max_size = max_size
52 self.ttl = ttl
53 self._cache: Dict[str, CacheEntry] = {}
54 self._lock = asyncio.Lock()
56 async def initialize(self) -> None:
57 """Initialize cache service."""
58 logger.info("Initializing resource cache")
59 # Start cleanup task
60 asyncio.create_task(self._cleanup_loop())
62 async def shutdown(self) -> None:
63 """Shutdown cache service."""
64 logger.info("Shutting down resource cache")
65 self.clear()
67 def get(self, key: str) -> Optional[Any]:
68 """Get value from cache.
70 Args:
71 key: Cache key
73 Returns:
74 Cached value or None if not found/expired
75 """
76 if key not in self._cache:
77 return None
79 entry = self._cache[key]
80 now = time.time()
82 # Check expiration
83 if now > entry.expires_at:
84 del self._cache[key]
85 return None
87 # Update access time
88 entry.last_access = now
89 return entry.value
91 def set(self, key: str, value: Any) -> None:
92 """Set value in cache.
94 Args:
95 key: Cache key
96 value: Value to cache
97 """
98 now = time.time()
100 # Check size limit
101 if len(self._cache) >= self.max_size:
102 # Remove least recently used
103 lru_key = min(self._cache.keys(), key=lambda k: self._cache[k].last_access)
104 del self._cache[lru_key]
106 # Add new entry
107 self._cache[key] = CacheEntry(value=value, expires_at=now + self.ttl, last_access=now)
109 def delete(self, key: str) -> None:
110 """Delete value from cache.
112 Args:
113 key: Cache key to delete
114 """
115 self._cache.pop(key, None)
117 def clear(self) -> None:
118 """Clear all cached entries."""
119 self._cache.clear()
121 async def _cleanup_loop(self) -> None:
122 """Background task to clean expired entries."""
123 while True:
124 try:
125 async with self._lock:
126 now = time.time()
127 expired = [key for key, entry in self._cache.items() if now > entry.expires_at]
128 for key in expired: 128 ↛ 129line 128 didn't jump to line 129 because the loop on line 128 never started
129 del self._cache[key]
131 if expired: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true
132 logger.debug(f"Cleaned {len(expired)} expired cache entries")
134 except Exception as e:
135 logger.error(f"Cache cleanup error: {e}")
137 await asyncio.sleep(60) # Run every minute