|
4 | 4 | # SPDX-License-Identifier: AGPL-3.0-or-later |
5 | 5 |
|
6 | 6 | import asyncio |
| 7 | +import collections |
7 | 8 | import contextlib |
8 | 9 | import io |
9 | 10 | import ipaddress |
@@ -240,7 +241,7 @@ def validate_expose_payload(self) -> Self: |
240 | 241 |
|
241 | 242 | EXAPP_CACHE: dict[str, ExApp] = {} |
242 | 243 | _EXAPP_INFLIGHT: dict[str, asyncio.Future[ExApp | None]] = {} |
243 | | -_EXAPP_NEGATIVE_CACHE: dict[str, float] = {} # exapp_id -> expiry monotonic timestamp |
| 244 | +_EXAPP_NEGATIVE_CACHE: collections.OrderedDict[str, float] = collections.OrderedDict() |
244 | 245 | _NEGATIVE_CACHE_TTL = 15.0 # seconds to cache "ExApp not found" results |
245 | 246 |
|
246 | 247 | SESSION_CACHE_LOCK = asyncio.Lock() |
@@ -594,6 +595,16 @@ async def _fetch_exapp_record(exapp_id: str) -> ExApp | None: |
594 | 595 | return exapp_record |
595 | 596 |
|
596 | 597 |
|
| 598 | +def _negative_cache_evict_expired() -> None: |
| 599 | + """Remove expired entries from the front of the ordered negative cache.""" |
| 600 | + now = time.monotonic() |
| 601 | + while _EXAPP_NEGATIVE_CACHE: |
| 602 | + _, expiry = next(iter(_EXAPP_NEGATIVE_CACHE.items())) |
| 603 | + if now < expiry: |
| 604 | + break |
| 605 | + _EXAPP_NEGATIVE_CACHE.popitem(last=False) |
| 606 | + |
| 607 | + |
597 | 608 | async def _get_or_fetch_exapp(exapp_id: str) -> ExApp | None: |
598 | 609 | """Get ExApp from cache, or fetch with request coalescing. |
599 | 610 |
|
@@ -632,6 +643,7 @@ async def _get_or_fetch_exapp(exapp_id: str) -> ExApp | None: |
632 | 643 | LOGGER.info("Received new ExApp record: %s", exapp_record) |
633 | 644 | EXAPP_CACHE[exapp_id] = exapp_record |
634 | 645 | else: |
| 646 | + _negative_cache_evict_expired() |
635 | 647 | _EXAPP_NEGATIVE_CACHE[exapp_id] = time.monotonic() + _NEGATIVE_CACHE_TTL |
636 | 648 | fut.set_result(exapp_record) |
637 | 649 | return exapp_record |
|
0 commit comments