Skip to content

Commit 6175248

Browse files
committed
Rebind the stat index when a later call names a different root
_ensure_stat_index bound to the first root/cache_root a process ever saw and returned immediately for every later call, for the life of the process. A read for a later root silently came from the first root's file, and at exit the accumulated in memory index, now holding entries for every root ever touched, was written back into the first root's file alone, poisoning it with paths from unrelated projects. Detect a switch to a different cache root, flush the outgoing root's own pending entries to its own file before switching, then load the new root fresh, so each root's file only ever holds that root's own entries. Also warn on the switch, so a library caller crossing project roots in one process sees it rather than losing the fastpath silently. Capture the resolved stat index path at bind time rather than deriving it fresh from the live out directory at flush time, since a caller following the documented pattern of setting the out directory then calling has already pointed it at the new root by the time the switch is noticed, and deriving it fresh then would flush the outgoing root's entries into the new root's file instead of its own. Fixes #3989.
1 parent e10df08 commit 6175248

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

‎graphify/cache.py‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ def _body_content(content: bytes) -> bytes:
205205
# (cache_root, #1774) — the two differ under --out and must not be conflated.
206206
_stat_index_anchor: Path | None = None
207207
_stat_index_dirty: bool = False
208+
_stat_index_atexit_registered: bool = False
209+
# The resolved on-disk path for the CURRENTLY bound root, captured at bind
210+
# time (#3989). A mid-process root switch must flush the OUTGOING root to
211+
# the file it was actually loaded from, not wherever the live _GRAPHIFY_OUT
212+
# happens to point by the time the switch is detected — a caller following
213+
# the documented one-root-per-call pattern (set _GRAPHIFY_OUT, then call)
214+
# has already moved it on to the NEW root before the switch is noticed.
215+
_stat_index_path: Path | None = None
208216

209217

210218
# Filesystem mtime granularity, in nanoseconds. A stat signature only proves a
@@ -328,18 +336,42 @@ def _stat_index_file(root: Path) -> Path:
328336

329337
def _ensure_stat_index(root: Path, cache_root: "Path | None" = None) -> None:
330338
global _stat_index, _stat_index_root, _stat_index_anchor, _stat_index_dirty
339+
global _stat_index_atexit_registered, _stat_index_path
340+
new_root = Path(cache_root if cache_root is not None else root).resolve()
331341
if _stat_index_root is not None:
332-
return
342+
if _stat_index_root == new_root:
343+
return
344+
# A later call in the same process named a DIFFERENT cache root
345+
# (#3989): the index was bound to the first root ever seen and never
346+
# re-bound, so every root after the first served (on read) whatever
347+
# the first root's file happened to contain, and (on write/exit)
348+
# deposited its own freshly-computed entries into the FIRST root's
349+
# file instead of its own — silently poisoning one project's
350+
# stat-index.json with paths from a completely different project.
351+
# Flush the outgoing root's own pending entries to its own file
352+
# before switching, then load the new root fresh, so each root's
353+
# file only ever holds that root's own entries.
354+
warnings.warn(
355+
f"stat index switched from cache root {str(_stat_index_root)!r} to "
356+
f"{str(new_root)!r} within one process; each root's "
357+
"stat-index.json now only reflects its own files, but library "
358+
"callers crossing project roots in one process should still use "
359+
"a fresh process per root to avoid losing the fastpath (#3989).",
360+
RuntimeWarning,
361+
stacklevel=3,
362+
)
363+
_flush_stat_index()
333364
# _stat_index_root determines the cache FILE location, so honoring an
334365
# explicit cache_root keeps detect()'s word-count cache under the requested
335366
# --out dir instead of polluting the scanned corpus with a stray
336367
# graphify-out/ (#1747). _stat_index_anchor is the separate KEY anchor:
337368
# in-memory keys stay absolute, but the on-disk index stores in-anchor keys
338369
# relative so a moved/cloned corpus still hits (#2199) — same load/save
339370
# re-anchoring the detect manifest uses.
340-
_stat_index_root = Path(cache_root if cache_root is not None else root).resolve()
371+
_stat_index_root = new_root
341372
_stat_index_anchor = Path(root).resolve()
342373
p = _stat_index_file(_stat_index_root)
374+
_stat_index_path = p
343375
_stat_index = {}
344376
if p.exists():
345377
try:
@@ -357,14 +389,23 @@ def _ensure_stat_index(root: Path, cache_root: "Path | None" = None) -> None:
357389
_stat_index[_stat_key_to_absolute(k, _stat_index_anchor)] = v
358390
except (json.JSONDecodeError, OSError):
359391
_stat_index = {}
360-
atexit.register(_flush_stat_index)
392+
_stat_index_dirty = False
393+
if not _stat_index_atexit_registered:
394+
atexit.register(_flush_stat_index)
395+
_stat_index_atexit_registered = True
361396

362397

363398
def _flush_stat_index() -> None:
364399
global _stat_index_dirty, _stat_index_root
365400
if not _stat_index_dirty or _stat_index_root is None:
366401
return
367-
p = _stat_index_file(_stat_index_root)
402+
# Use the path captured when this root was bound (#3989), not a fresh
403+
# _stat_index_file(_stat_index_root) call: a mid-process root switch runs
404+
# this to flush the OUTGOING root, by which point a caller following the
405+
# documented set-_GRAPHIFY_OUT-then-call pattern has already pointed
406+
# _GRAPHIFY_OUT at the NEW root, and re-deriving here would flush the old
407+
# root's entries into the new root's file instead of its own.
408+
p = _stat_index_path if _stat_index_path is not None else _stat_index_file(_stat_index_root)
368409
# Build the on-disk form (#2199): prune entries whose file is gone (the
369410
# index otherwise grows without bound), then store in-anchor keys as
370411
# forward-slash relative paths so the index survives a corpus move/clone.

0 commit comments

Comments
 (0)