Skip to content

Commit eccdd39

Browse files
committed
Limit cache size
and evict based on request expiration deadline.
1 parent 9cfb7cf commit eccdd39

File tree

3 files changed

+68
-1
lines changed

3 files changed

+68
-1
lines changed

src/cvec/cvec.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from urllib.parse import urlencode, urljoin
1313
from urllib.request import Request, urlopen
1414

15-
from cvec.http_cache import CacheEntry, parse_max_age
15+
from cvec.http_cache import MAX_CACHE_ENTRIES, CacheEntry, parse_max_age
1616

1717
from cvec.models.agent_post import AgentPost, AgentPostRecommendation, AgentPostTag
1818
from cvec.models.eav_column import EAVColumn
@@ -152,6 +152,11 @@ def _process_response(self, response: Any, url: str, method: str) -> Any:
152152
cache_control = response.headers.get("Cache-Control", "")
153153
max_age = parse_max_age(cache_control)
154154
if max_age is not None:
155+
if url not in self._cache and len(self._cache) >= MAX_CACHE_ENTRIES:
156+
worst_url = min(
157+
self._cache, key=lambda u: self._cache[u].expires_at
158+
)
159+
del self._cache[worst_url]
155160
etag = response.headers.get("ETag", "") or None
156161
self._cache[url] = CacheEntry(
157162
data=parsed,

src/cvec/http_cache.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from typing import Any, Optional
55

66

7+
MAX_CACHE_ENTRIES = 100
8+
9+
710
@dataclass
811
class CacheEntry:
912
"""A cached HTTP response."""
@@ -13,6 +16,11 @@ class CacheEntry:
1316
max_age: int
1417
stored_at: float
1518

19+
@property
20+
def expires_at(self) -> float:
21+
"""Monotonic time when this entry expires."""
22+
return self.stored_at + self.max_age
23+
1624

1725
def parse_max_age(header: Optional[str]) -> Optional[int]:
1826
"""Parse max-age value from a Cache-Control header.

tests/test_http_cache.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def test_no_etag(self) -> None:
8585
entry = CacheEntry(data=[], etag=None, max_age=60, stored_at=0.0)
8686
assert entry.etag is None
8787

88+
def test_expires_at(self) -> None:
89+
entry = CacheEntry(data=[], etag=None, max_age=300, stored_at=100.0)
90+
assert entry.expires_at == 400.0
91+
8892

8993
class TestHttpCache:
9094
"""Integration tests for caching in _make_request."""
@@ -351,3 +355,53 @@ def test_different_urls_cached_separately(
351355
client.get_metric_data(names=["m1"])
352356

353357
assert len(client._cache) == 2
358+
359+
@patch.object(CVec, "_login_with_supabase", return_value=None)
360+
@patch.object(
361+
CVec,
362+
"_fetch_config",
363+
autospec=True,
364+
side_effect=mock_fetch_config_side_effect,
365+
)
366+
@patch("cvec.cvec.urlopen")
367+
def test_cache_evicts_when_full(
368+
self,
369+
mock_urlopen: Any,
370+
mock_fetch_key: Any,
371+
mock_login: Any,
372+
) -> None:
373+
"""When cache is full, the entry with earliest expiration is evicted."""
374+
client = _create_client()
375+
376+
now = time.monotonic()
377+
378+
# Pre-fill cache to max size with entries that expire at different times
379+
for i in range(100):
380+
url = f"https://test.example.com/api/item/{i}"
381+
client._cache[url] = CacheEntry(
382+
data={"id": i},
383+
etag=f'"etag{i}"',
384+
max_age=300,
385+
# Entry 50 expires first (stored earliest)
386+
stored_at=now - 200 if i == 50 else now,
387+
)
388+
389+
assert len(client._cache) == 100
390+
391+
# Add one more entry via a real request
392+
data = [{"id": 999, "name": "new_metric"}]
393+
mock_response = _make_mock_response(
394+
json.dumps(data).encode("utf-8"),
395+
etag='"etag_new"',
396+
cache_control="max-age=300",
397+
)
398+
mock_urlopen.return_value = mock_response
399+
400+
client.get_metrics()
401+
402+
# Cache should still be at 100 (evicted one to make room)
403+
assert len(client._cache) == 100
404+
# Entry 50 (earliest expiration) should have been evicted
405+
assert "https://test.example.com/api/item/50" not in client._cache
406+
# New entry should be present
407+
assert any("metrics" in url for url in client._cache)

0 commit comments

Comments
 (0)