Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit e4ca5a0

Browse files
authored
Merge pull request #139 from Genmin/fix/api-key-validation-cache
perf: cache API key validation
2 parents c36fefe + eb2f824 commit e4ca5a0

1 file changed

Lines changed: 65 additions & 5 deletions

File tree

src/database/api_key_store.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import logging
55
import secrets
66
import string
7+
import time
8+
from collections import OrderedDict
79
from datetime import datetime
10+
from threading import RLock
811
from typing import List, Optional, Dict, Any
912

1013
from src.config import settings
@@ -18,6 +21,11 @@
1821
# In-memory fallback
1922
_in_memory_api_keys: Dict[str, Dict[str, Any]] = {}
2023

24+
VALIDATION_CACHE_TTL_SECONDS = 120
25+
VALIDATION_CACHE_MAX_SIZE = 2048
26+
_validation_cache: OrderedDict[str, tuple[float, Dict[str, Any]]] = OrderedDict()
27+
_validation_cache_lock = RLock()
28+
2129

2230
class APIKeyStore:
2331
"""MongoDB-backed storage for API key management with in-memory fallback."""
@@ -79,6 +87,41 @@ def _hash_key(self, key: str) -> str:
7987
"""Create SHA-256 hash of the API key."""
8088
return hashlib.sha256(key.encode()).hexdigest()
8189

90+
def _clear_validation_cache(self) -> None:
91+
"""Clear cached API key validation results."""
92+
with _validation_cache_lock:
93+
_validation_cache.clear()
94+
95+
def _get_cached_validation(self, key_hash: str) -> Optional[Dict[str, Any]]:
96+
"""Return a cached validation result when it is still fresh."""
97+
with _validation_cache_lock:
98+
cached = _validation_cache.get(key_hash)
99+
if not cached:
100+
return None
101+
102+
expires_at, key_doc = cached
103+
if expires_at <= time.monotonic():
104+
_validation_cache.pop(key_hash, None)
105+
return None
106+
107+
result = dict(key_doc)
108+
109+
result["last_used"] = datetime.utcnow()
110+
return result
111+
112+
def _cache_validation(self, key_hash: str, key_doc: Dict[str, Any]) -> None:
113+
"""Cache a sanitized active API key document."""
114+
with _validation_cache_lock:
115+
if key_hash in _validation_cache:
116+
_validation_cache.pop(key_hash, None)
117+
while len(_validation_cache) >= VALIDATION_CACHE_MAX_SIZE:
118+
_validation_cache.popitem(last=False)
119+
120+
_validation_cache[key_hash] = (
121+
time.monotonic() + VALIDATION_CACHE_TTL_SECONDS,
122+
dict(key_doc),
123+
)
124+
82125
def create_api_key(
83126
self,
84127
user_id: str,
@@ -174,6 +217,10 @@ def validate_api_key(self, key: str) -> Optional[Dict[str, Any]]:
174217
return result
175218
return None
176219

220+
cached_doc = self._get_cached_validation(key_hash)
221+
if cached_doc:
222+
return cached_doc
223+
177224
try:
178225
key_doc = self.api_keys.find_one({
179226
"key_hash": key_hash,
@@ -185,10 +232,15 @@ def validate_api_key(self, key: str) -> Optional[Dict[str, Any]]:
185232
{"_id": key_doc["_id"]},
186233
{"$set": {"last_used": now}}
187234
)
188-
key_doc["last_used"] = now
189-
key_doc["id"] = str(key_doc.pop("_id"))
235+
key_doc = {
236+
**key_doc,
237+
"id": str(key_doc["_id"]),
238+
"last_used": now,
239+
}
240+
key_doc.pop("_id", None)
190241
key_doc.pop("key_hash", None)
191-
return key_doc
242+
self._cache_validation(key_hash, key_doc)
243+
return dict(key_doc) if key_doc else None
192244
except Exception as e:
193245
logger.error(f"Database error validating API key: {e}")
194246
return None
@@ -199,6 +251,7 @@ def revoke_api_key(self, user_id: str, key_id: str) -> bool:
199251
if key_id in _in_memory_api_keys:
200252
if _in_memory_api_keys[key_id].get("user_id") == user_id:
201253
_in_memory_api_keys[key_id]["is_active"] = False
254+
self._clear_validation_cache()
202255
return True
203256
return False
204257

@@ -208,7 +261,10 @@ def revoke_api_key(self, user_id: str, key_id: str) -> bool:
208261
{"_id": ObjectId(key_id), "user_id": user_id},
209262
{"$set": {"is_active": False}}
210263
)
211-
return result.modified_count > 0
264+
success = result.modified_count > 0
265+
if success:
266+
self._clear_validation_cache()
267+
return success
212268
except Exception as e:
213269
logger.error(f"Failed to revoke API key {key_id}: {e}")
214270
return False
@@ -224,6 +280,7 @@ def update_api_key_name(
224280
if key_id in _in_memory_api_keys:
225281
if _in_memory_api_keys[key_id].get("user_id") == user_id:
226282
_in_memory_api_keys[key_id]["name"] = new_name
283+
self._clear_validation_cache()
227284
return True
228285
return False
229286

@@ -233,7 +290,10 @@ def update_api_key_name(
233290
{"_id": ObjectId(key_id), "user_id": user_id},
234291
{"$set": {"name": new_name}}
235292
)
236-
return result.modified_count > 0
293+
success = result.modified_count > 0
294+
if success:
295+
self._clear_validation_cache()
296+
return success
237297
except Exception as e:
238298
logger.error(f"Failed to update API key name {key_id}: {e}")
239299
return False

0 commit comments

Comments
 (0)