Coverage for src / agent / services / maven / cache.py: 100%
77 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
1# Copyright 2025-2026 Microsoft Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""TTL-based in-memory cache service for Maven API responses.
17This module provides a simple, efficient caching mechanism for Maven Central
18API responses with configurable TTL support. Cache keys follow patterns like:
19- metadata:{group_id}:{artifact_id}
20- versions:{group_id}:{artifact_id}
21- exists:{group_id}:{artifact_id}:{version}
22"""
24import logging
25import time
26from dataclasses import dataclass
27from typing import Any
29logger = logging.getLogger(__name__)
32@dataclass
33class CacheEntry:
34 """Single cache entry with value and expiration time."""
36 value: Any
37 expires_at: float
40class MavenCacheService:
41 """In-memory cache with TTL support for Maven API responses.
43 Thread-safe cache implementation using simple dictionary storage.
44 Expired entries are cleaned up on access to prevent memory leaks.
46 Default TTLs:
47 - Metadata: 3600 seconds (1 hour)
48 - Search results: 900 seconds (15 minutes)
49 - Artifact existence: 3600 seconds (1 hour)
50 """
52 # Default TTL values in seconds
53 DEFAULT_METADATA_TTL = 3600 # 1 hour
54 DEFAULT_SEARCH_TTL = 900 # 15 minutes
55 DEFAULT_EXISTS_TTL = 3600 # 1 hour
57 def __init__(self, default_ttl: int = 3600) -> None:
58 """Initialize cache service.
60 Args:
61 default_ttl: Default TTL in seconds for cache entries (default: 1 hour)
62 """
63 self._cache: dict[str, CacheEntry] = {}
64 self._default_ttl = default_ttl
65 logger.debug("MavenCacheService initialized with default TTL: %ds", default_ttl)
67 def get(self, key: str) -> Any | None:
68 """Get value from cache if not expired.
70 Args:
71 key: Cache key to look up
73 Returns:
74 Cached value if exists and not expired, None otherwise
75 """
76 entry = self._cache.get(key)
77 if entry is None:
78 return None
80 if time.time() > entry.expires_at:
81 # Entry expired, remove it
82 del self._cache[key]
83 logger.debug("Cache entry expired: %s", key)
84 return None
86 logger.debug("Cache hit: %s", key)
87 return entry.value
89 def set(self, key: str, value: Any, ttl: int | None = None) -> None:
90 """Set value in cache with TTL.
92 Args:
93 key: Cache key
94 value: Value to cache
95 ttl: TTL in seconds (uses default if not specified)
96 """
97 ttl_seconds = ttl if ttl is not None else self._default_ttl
98 expires_at = time.time() + ttl_seconds
99 self._cache[key] = CacheEntry(value=value, expires_at=expires_at)
100 logger.debug("Cache set: %s (TTL: %ds)", key, ttl_seconds)
102 def delete(self, key: str) -> bool:
103 """Delete a specific cache entry.
105 Args:
106 key: Cache key to delete
108 Returns:
109 True if entry was deleted, False if not found
110 """
111 if key in self._cache:
112 del self._cache[key]
113 logger.debug("Cache entry deleted: %s", key)
114 return True
115 return False
117 def clear(self) -> int:
118 """Clear all cache entries.
120 Returns:
121 Number of entries cleared
122 """
123 count = len(self._cache)
124 self._cache.clear()
125 logger.debug("Cache cleared: %d entries", count)
126 return count
128 def cleanup_expired(self) -> int:
129 """Remove all expired entries from cache.
131 Returns:
132 Number of expired entries removed
133 """
134 current_time = time.time()
135 expired_keys = [
136 key for key, entry in self._cache.items() if current_time > entry.expires_at
137 ]
139 for key in expired_keys:
140 del self._cache[key]
142 if expired_keys:
143 logger.debug("Cleaned up %d expired cache entries", len(expired_keys))
145 return len(expired_keys)
147 def size(self) -> int:
148 """Get current cache size.
150 Returns:
151 Number of entries in cache (including expired ones)
152 """
153 return len(self._cache)
155 # Convenience methods for Maven-specific cache operations
157 def get_metadata(self, group_id: str, artifact_id: str) -> Any | None:
158 """Get cached Maven metadata.
160 Args:
161 group_id: Maven group ID
162 artifact_id: Maven artifact ID
164 Returns:
165 Cached metadata or None
166 """
167 key = f"metadata:{group_id}:{artifact_id}"
168 return self.get(key)
170 def set_metadata(self, group_id: str, artifact_id: str, metadata: Any) -> None:
171 """Cache Maven metadata.
173 Args:
174 group_id: Maven group ID
175 artifact_id: Maven artifact ID
176 metadata: Metadata to cache
177 """
178 key = f"metadata:{group_id}:{artifact_id}"
179 self.set(key, metadata, ttl=self.DEFAULT_METADATA_TTL)
181 def get_versions(self, group_id: str, artifact_id: str) -> list[str] | None:
182 """Get cached version list.
184 Args:
185 group_id: Maven group ID
186 artifact_id: Maven artifact ID
188 Returns:
189 Cached version list or None
190 """
191 key = f"versions:{group_id}:{artifact_id}"
192 return self.get(key)
194 def set_versions(self, group_id: str, artifact_id: str, versions: list[str]) -> None:
195 """Cache version list.
197 Args:
198 group_id: Maven group ID
199 artifact_id: Maven artifact ID
200 versions: List of versions to cache
201 """
202 key = f"versions:{group_id}:{artifact_id}"
203 self.set(key, versions, ttl=self.DEFAULT_METADATA_TTL)
205 def get_exists(
206 self,
207 group_id: str,
208 artifact_id: str,
209 version: str,
210 packaging: str = "jar",
211 classifier: str | None = None,
212 ) -> bool | None:
213 """Get cached artifact existence check.
215 Args:
216 group_id: Maven group ID
217 artifact_id: Maven artifact ID
218 version: Version string
219 packaging: Package type
220 classifier: Optional classifier
222 Returns:
223 Cached existence result or None
224 """
225 key = f"exists:{group_id}:{artifact_id}:{version}:{packaging}:{classifier}"
226 return self.get(key)
228 def set_exists(
229 self,
230 group_id: str,
231 artifact_id: str,
232 version: str,
233 exists: bool,
234 packaging: str = "jar",
235 classifier: str | None = None,
236 ) -> None:
237 """Cache artifact existence check.
239 Args:
240 group_id: Maven group ID
241 artifact_id: Maven artifact ID
242 version: Version string
243 exists: Whether artifact exists
244 packaging: Package type
245 classifier: Optional classifier
246 """
247 key = f"exists:{group_id}:{artifact_id}:{version}:{packaging}:{classifier}"
248 self.set(key, exists, ttl=self.DEFAULT_EXISTS_TTL)
250 def get_search(self, query: str) -> Any | None:
251 """Get cached search results.
253 Args:
254 query: Search query string
256 Returns:
257 Cached search results or None
258 """
259 key = f"search:{query}"
260 return self.get(key)
262 def set_search(self, query: str, results: Any) -> None:
263 """Cache search results.
265 Args:
266 query: Search query string
267 results: Search results to cache
268 """
269 key = f"search:{query}"
270 self.set(key, results, ttl=self.DEFAULT_SEARCH_TTL)