large-bucket-sync / app /read_model.py
lvwerra's picture
lvwerra HF Staff
Upload folder using huggingface_hub
719a7ec verified
Raw
History Blame Contribute Delete
13 kB
"""In-process read model over the central bucket (Β§16.1).
Two layers, per central-bucket folder:
- **Listing cache** β€” the folder's tree listing, refreshed at most once per
``LISTING_TTL_S`` behind a per-folder lock (single-flight): any number of
concurrent readers costs at most one bucket listing per TTL window.
- **Content cache** β€” parsed ``{frontmatter, body}`` per file, keyed by the
listing's ``xet_hash`` so byte-identical files (inbox copies) share one
cached entry. Bounded by ``CONTENT_CACHE_MAX_BYTES`` with LRU eviction;
eviction means a refetch, never an error. Cold misses are fetched in one
**batch** download, not per file.
Coherence: the Space is the only writer to the central bucket (Β§2), so every
API write is inserted synchronously (``write_through``) β€” agents always
observe their own writes immediately, independent of TTL. Locally written
entries live in an overlay merged over bucket listings for a grace window, so
a lagging bucket listing can never make a fresh write disappear. The TTL
exists only to pick up out-of-band admin edits (verification verdicts, force
re-registrations); the per-file hash check then refreshes exactly the changed
entries, so mutable files need no special handling.
All state here is cache β€” restart-safe by loss (Β§1).
"""
from __future__ import annotations
import json
import logging
import re
import threading
import time
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Any, Callable
from app.config import Settings
from app.frontmatter import parse
from app.hub import HubClient, ListedFile
from app.naming import VERIFICATION_STATUS_PATH
log = logging.getLogger(__name__)
_README_RE = re.compile(r"(?:^|/)README\.md$", re.IGNORECASE)
# How long a write-through entry shadows the bucket before we trust the bucket
# listing to have caught up. Generous; a write normally appears immediately.
_OVERLAY_GRACE_S = 300.0
@dataclass
class Record:
filename: str
path: str
frontmatter: dict[str, Any]
body: str
size: int
parse_error: bool = False
@dataclass
class _Folder:
files: dict[str, ListedFile] = field(default_factory=dict)
fetched_at: float = float("-inf")
overlay: dict[str, tuple[ListedFile, float]] = field(default_factory=dict)
lock: threading.Lock = field(default_factory=threading.Lock)
def _safe_parse(raw: bytes) -> tuple[dict[str, Any], str, bool]:
"""Parse a bucket file, never raising: a malformed historical file must
degrade to an empty-frontmatter record, not 4xx/5xx a GET."""
try:
text = raw.decode("utf-8")
except UnicodeDecodeError:
return {}, raw.decode("utf-8", errors="replace"), True
try:
fm, body = parse(text)
except Exception:
return {}, text, True
return fm, body, False
class ReadModel:
def __init__(
self,
hub: HubClient,
settings: Settings,
clock: Callable[[], float] = time.monotonic,
):
self._hub = hub
self._settings = settings
self._clock = clock
self._folders: dict[str, _Folder] = {}
self._folders_lock = threading.Lock()
# Parsed content keyed by xet_hash: (frontmatter, body, size, parse_error).
self._content: OrderedDict[str, tuple[dict, str, int, bool]] = OrderedDict()
self._content_bytes = 0
# Write-through entries whose xet_hash isn't known yet, keyed by path.
self._local: dict[str, tuple[dict, str, int, float]] = {}
self._verification: tuple[str, dict[str, str]] | None = None
self._content_lock = threading.Lock()
# ───────────────────────── listings ─────────────────────────
def _folder(self, folder: str) -> _Folder:
with self._folders_lock:
return self._folders.setdefault(folder, _Folder())
def listing(self, folder: str) -> list[ListedFile]:
"""The folder's current listing: TTL-cached bucket truth merged with
the local write-through overlay (overlay fills gaps, never overrides)."""
f = self._folder(folder)
with f.lock:
now = self._clock()
if now - f.fetched_at >= self._settings.listing_ttl_s:
fresh = self._hub.list_central_dir(folder)
if not fresh and f.files:
# The hub flattens listing errors to []; nothing is ever
# deleted from these folders, so an empty result for a
# previously non-empty folder is a transient failure.
log.warning(
"listing(%s) came back empty; keeping %d cached entries",
folder, len(f.files),
)
else:
f.files = {e.rel_path: e for e in fresh}
f.fetched_at = now
f.overlay = {
p: (e, ts)
for p, (e, ts) in f.overlay.items()
if p not in f.files and now - ts < _OVERLAY_GRACE_S
}
merged = dict(f.files)
for p, (e, _ts) in f.overlay.items():
merged.setdefault(p, e)
return list(merged.values())
def _md_entries(self, folder: str) -> list[ListedFile]:
return [
e
for e in self.listing(folder)
if e.rel_path.endswith(".md") and not _README_RE.search(e.rel_path)
]
# ───────────────────────── records ─────────────────────────
def records(self, folder: str) -> list[Record]:
"""Parsed records for every .md file under ``folder`` (READMEs
excluded), ascending by filename. Cold misses are batch-fetched."""
out = self._resolve_many(self._md_entries(folder))
return [out[p] for p in sorted(out)]
def records_for(self, folder: str, paths: list[str]) -> dict[str, Record]:
"""Resolve specific files from ``folder``'s listing through the content
cache, keyed by rel_path. For files ``records`` excludes by convention
(READMEs) or selective reads over a tree listing (taskforces, Β§18).
Unlisted paths are silently absent from the result."""
by_path = {e.rel_path: e for e in self.listing(folder)}
return self._resolve_many([by_path[p] for p in paths if p in by_path])
def _resolve_many(self, entries: list[ListedFile]) -> dict[str, Record]:
out: dict[str, Record] = {}
misses: list[ListedFile] = []
with self._content_lock:
for e in entries:
rec = self._resolve_cached(e)
if rec is not None:
out[e.rel_path] = rec
else:
misses.append(e)
if misses:
fetched = self._hub.download_many(
self._settings.central_bucket, [e.rel_path for e in misses]
)
with self._content_lock:
for e in misses:
raw = fetched.get(e.rel_path)
if raw is None:
continue # transient download failure; heals next pass
out[e.rel_path] = self._insert(e, raw)
return out
def record(self, folder: str, filename: str) -> Record | None:
"""One file, resolved through the cache; None if it isn't listed."""
path = f"{folder}/{filename}"
entry = next((e for e in self.listing(folder) if e.rel_path == path), None)
if entry is None:
return None
with self._content_lock:
rec = self._resolve_cached(entry)
if rec is not None:
return rec
raw = self._hub.download_many(self._settings.central_bucket, [path]).get(path)
if raw is None:
return None
with self._content_lock:
return self._insert(entry, raw)
def _resolve_cached(self, e: ListedFile) -> Record | None:
"""Caller holds ``_content_lock``."""
filename = e.rel_path.rsplit("/", 1)[-1]
if e.xet_hash and e.xet_hash in self._content:
self._content.move_to_end(e.xet_hash)
fm, body, size, perr = self._content[e.xet_hash]
return Record(filename, e.rel_path, fm, body, size, perr)
if e.rel_path in self._local:
fm, body, size, _ts = self._local[e.rel_path]
return Record(filename, e.rel_path, fm, body, size, False)
return None
def _insert(self, e: ListedFile, raw: bytes) -> Record:
"""Caller holds ``_content_lock``."""
fm, body, perr = _safe_parse(raw)
if e.xet_hash:
if e.xet_hash not in self._content:
self._content[e.xet_hash] = (fm, body, len(raw), perr)
self._content_bytes += len(raw)
while (
self._content_bytes > self._settings.content_cache_max_bytes
and len(self._content) > 1
):
_, (_f, _b, sz, _p) = self._content.popitem(last=False)
self._content_bytes -= sz
else:
self._content.move_to_end(e.xet_hash)
filename = e.rel_path.rsplit("/", 1)[-1]
return Record(filename, e.rel_path, fm, body, len(raw), perr)
# ───────────────────────── write-through ─────────────────────────
def write_through(
self, path: str, frontmatter: dict, body: str, size: int,
folder: str | None = None,
) -> None:
"""Insert a just-written central-bucket file so read-after-write is
exact regardless of listing TTL. Call right after the bucket write.
``folder`` pins which folder cache gets the listing overlay when it is
not the file's immediate parent β€” taskforce files live under one shared
``taskforces`` tree listing whatever their subdirectory (Β§18.4)."""
if folder is None:
folder, _, _filename = path.rpartition("/")
f = self._folder(folder)
now = self._clock()
with f.lock:
f.overlay[path] = (ListedFile(rel_path=path, size=size, xet_hash=None), now)
with self._content_lock:
self._local[path] = (frontmatter, body, size, now)
stale = [
p for p, (_f, _b, _s, ts) in self._local.items()
if now - ts >= _OVERLAY_GRACE_S
]
for p in stale:
del self._local[p]
# ───────────────────────── derived views ─────────────────────────
def registered_agents(self) -> set[str]:
return {
e.rel_path.rsplit("/", 1)[-1].removesuffix(".md")
for e in self._md_entries("agents")
}
def invalidate_verification_index(self) -> None:
"""Drop the cached verification index after the Space itself rewrites
it (automated verdicts, Β§5.7) β€” that write is no longer an out-of-band
admin edit, so it must not wait out the listing TTL. The next
``verification_index()`` call refetches the file (one download)."""
with self._content_lock:
self._verification = None
def verification_index(self) -> dict[str, str]:
"""Parsed ``results/verification_status.json``, cached by its listing
hash. Absent or unreadable β†’ {} (every result then reads as pending β€”
the truthful default for an unreviewed result)."""
entry = next(
(e for e in self.listing("results") if e.rel_path == VERIFICATION_STATUS_PATH),
None,
)
if entry is None:
return {}
with self._content_lock:
if (
entry.xet_hash
and self._verification is not None
and self._verification[0] == entry.xet_hash
):
return self._verification[1]
raw = self._hub.download_many(
self._settings.central_bucket, [VERIFICATION_STATUS_PATH]
).get(VERIFICATION_STATUS_PATH)
if raw is None:
return {}
try:
data = json.loads(raw.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
log.error("verification index unparseable: %s", exc)
return {}
if not isinstance(data, dict):
log.error("verification index is not a JSON object")
return {}
index = {str(k): str(v) for k, v in data.items()}
if entry.xet_hash:
with self._content_lock:
self._verification = (entry.xet_hash, index)
return index