arozantsev's picture
Deploy simready-assets run 20260928-ed4eb73 (GPU variant)
1079f5b verified
Raw History Blame Contribute Delete
5.5 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""TTL + LRU cache with single-flight loading (plan §5.2).
Single-flight is not optional here. A crawl ``Enumerate``s and then ``Stat``s
every item, and a thumbnail probe fires up to four ``Stat``s per asset for
candidate ``.thumbs`` paths — all against the same folder, concurrently. Without
collapsing concurrent loads of one key onto a single upstream request, laziness
turns one preload into a burst of duplicates.
"""
from __future__ import annotations
import asyncio
import time
from collections import OrderedDict
from collections.abc import Awaitable, Callable, Hashable
from dataclasses import dataclass, field
from typing import Generic, TypeVar
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")
class Missing:
"""A cached negative result.
Distinct from ``None`` so a miss can be cached and served without re-hitting
HuggingFace: the thumbnail probe generates a steady stream of legitimate
misses that would otherwise be upstream traffic every time.
"""
__slots__ = ()
def __bool__(self) -> bool:
return False
MISSING = Missing()
@dataclass
class CacheStats:
hits: int = 0
misses: int = 0
loads: int = 0
negative_hits: int = 0
evictions: int = 0
@property
def hit_rate(self) -> float:
total = self.hits + self.misses
return self.hits / total if total else 0.0
@dataclass
class _Entry(Generic[V]):
value: V | Missing
expires_at: float
class SingleFlightCache(Generic[K, V]):
"""Async cache: at most one in-flight load per key, TTL + LRU eviction."""
def __init__(
self,
*,
ttl: float,
negative_ttl: float,
max_size: int,
clock: Callable[[], float] = time.monotonic,
) -> None:
self._ttl = ttl
self._negative_ttl = negative_ttl
self._max_size = max_size
self._clock = clock
self._entries: OrderedDict[K, _Entry[V]] = OrderedDict()
self._inflight: dict[K, asyncio.Future[V | Missing]] = {}
self.stats = CacheStats()
def _get_live(self, key: K) -> _Entry[V] | None:
entry = self._entries.get(key)
if entry is None:
return None
if entry.expires_at <= self._clock():
del self._entries[key]
return None
self._entries.move_to_end(key)
return entry
def peek(self, key: K) -> V | Missing | None:
"""Return a cached value without loading. ``None`` means "not cached"."""
entry = self._get_live(key)
return entry.value if entry else None
def put(self, key: K, value: V | Missing) -> None:
ttl = self._negative_ttl if isinstance(value, Missing) else self._ttl
self._entries[key] = _Entry(value=value, expires_at=self._clock() + ttl)
self._entries.move_to_end(key)
while len(self._entries) > self._max_size:
self._entries.popitem(last=False)
self.stats.evictions += 1
async def get(self, key: K, loader: Callable[[], Awaitable[V | Missing]]) -> V | Missing:
"""Return the cached value, or load it — once, however many callers race."""
entry = self._get_live(key)
if entry is not None:
self.stats.hits += 1
if isinstance(entry.value, Missing):
self.stats.negative_hits += 1
return entry.value
self.stats.misses += 1
inflight = self._inflight.get(key)
if inflight is not None:
# Someone else is already loading this key: wait for their result
# rather than issuing a duplicate upstream request.
return await asyncio.shield(inflight)
future: asyncio.Future[V | Missing] = asyncio.get_running_loop().create_future()
self._inflight[key] = future
try:
self.stats.loads += 1
value = await loader()
except BaseException as exc:
# Failures are not cached — a transient 503 must not pin a miss for
# the whole TTL.
if not future.done():
future.set_exception(exc)
raise
else:
self.put(key, value)
if not future.done():
future.set_result(value)
return value
finally:
self._inflight.pop(key, None)
# Nobody may be awaiting a failed future; keep the loop quiet about it.
if future.done() and future.exception() is not None:
future.exception()
def clear(self) -> None:
self._entries.clear()
def __len__(self) -> int:
return len(self._entries)
@dataclass
class TokenBucket:
"""Simple rate limiter for upstream calls (plan §8.5)."""
rate: float
capacity: float
_tokens: float = field(default=0.0, init=False)
_updated: float = field(default=0.0, init=False)
def __post_init__(self) -> None:
self._tokens = self.capacity
self._updated = time.monotonic()
async def acquire(self) -> None:
while True:
now = time.monotonic()
self._tokens = min(self.capacity, self._tokens + (now - self._updated) * self.rate)
self._updated = now
if self._tokens >= 1:
self._tokens -= 1
return
await asyncio.sleep((1 - self._tokens) / self.rate)