Skip to content

Commit ba1282e

Browse files
committed
fixup! refactor(core.utils): stat-guard atime touch; suppress platformdirs Windows Cache leaf
1 parent 41101eb commit ba1282e

2 files changed

Lines changed: 75 additions & 9 deletions

File tree

cuda_core/cuda/core/utils/_program_cache.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -848,8 +848,11 @@ def _default_cache_dir() -> Path:
848848
Delegates to :mod:`platformdirs`, which encodes the per-platform
849849
rules canonically (and tracks corner cases like ``FOLDERID_*`` on
850850
Windows that an ad-hoc reimplementation would miss).
851+
``opinion=False`` suppresses the extra ``Cache`` component
852+
platformdirs would otherwise insert on Windows, keeping the layout
853+
identical across platforms (``<root>/cuda-python/program-cache``).
851854
"""
852-
return platformdirs.user_cache_path("cuda-python", appauthor=False) / "program-cache"
855+
return platformdirs.user_cache_path("cuda-python", appauthor=False, opinion=False) / "program-cache"
853856

854857

855858
def _replace_with_sharing_retry(tmp_path: Path, target: Path) -> bool:
@@ -918,8 +921,9 @@ def _stat_and_read_with_sharing_retry(path: Path) -> tuple[os.stat_result, bytes
918921
raise FileNotFoundError(path) from last_exc
919922

920923

921-
def _touch_atime(path: Path, st: os.stat_result) -> None:
922-
"""Bump ``path``'s atime to "now", preserving its mtime.
924+
def _touch_atime(path: Path, st_before: os.stat_result) -> None:
925+
"""Bump ``path``'s atime to "now", preserving its mtime, iff the
926+
file's stat still matches ``st_before``.
923927
924928
Eviction sorts by ``st_atime`` so reads must reliably refresh atime
925929
regardless of OS or filesystem default behavior:
@@ -933,12 +937,31 @@ def _touch_atime(path: Path, st: os.stat_result) -> None:
933937
* ``noatime``-mounted filesystems disable updates entirely.
934938
935939
Calling ``os.utime`` with explicit times bypasses all of the above
936-
and writes atime directly. Best-effort: any ``OSError`` (read-only
937-
mount, restrictive ACLs, ...) is swallowed -- size enforcement
938-
still bounds the cache, but eviction degrades toward FIFO.
940+
and writes atime directly. The stat-guard is critical: if another
941+
process ``os.replace``-d a fresh entry into ``path`` between the
942+
read and this touch, blindly applying ``st_before.st_mtime_ns``
943+
would roll the new entry's mtime back to the old value and confuse
944+
the eviction stat-guard (which checks ``(ino, size, mtime_ns)``)
945+
into deleting a freshly-committed file. A mismatch means the racing
946+
writer's reader will refresh atime when it next reads, so skipping
947+
here is safe.
948+
949+
Best-effort: any ``OSError`` (read-only mount, restrictive ACLs,
950+
...) is swallowed -- size enforcement still bounds the cache, but
951+
eviction degrades toward FIFO.
939952
"""
953+
try:
954+
st_now = path.stat()
955+
except OSError:
956+
return
957+
if (st_now.st_ino, st_now.st_size, st_now.st_mtime_ns) != (
958+
st_before.st_ino,
959+
st_before.st_size,
960+
st_before.st_mtime_ns,
961+
):
962+
return
940963
with contextlib.suppress(OSError):
941-
os.utime(path, ns=(time.time_ns(), st.st_mtime_ns))
964+
os.utime(path, ns=(time.time_ns(), st_before.st_mtime_ns))
942965

943966

944967
def _prune_if_stat_unchanged(path: Path, st_before: os.stat_result) -> None:

cuda_core/tests/test_program_cache.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,40 @@ def test_filestream_cache_prune_only_if_stat_unchanged(tmp_path):
11691169
assert not path.exists()
11701170

11711171

1172+
def test_filestream_cache_touch_atime_only_if_stat_unchanged(tmp_path):
1173+
"""The atime-touch is also stat-guarded so a racing rewriter's freshly
1174+
replaced file does NOT get its mtime rolled back to the previous
1175+
entry's value. Without the guard, the eviction stat-check (which keys
1176+
on (ino, size, mtime_ns)) would mistake the replacement for the old
1177+
entry and delete a just-committed file."""
1178+
from cuda.core.utils import FileStreamProgramCache
1179+
from cuda.core.utils._program_cache import _touch_atime
1180+
1181+
same_size_bytes = b"v" * 64
1182+
with FileStreamProgramCache(tmp_path / "fc") as cache:
1183+
cache[b"k"] = same_size_bytes
1184+
path = cache._path_for_key(b"k")
1185+
stale_stat = path.stat()
1186+
# Concurrent writer replaces with same-size payload (same st_size,
1187+
# different ino/mtime) -- this is the dangerous case: ino and
1188+
# mtime differ, only the stat-guard saves us.
1189+
time.sleep(0.02)
1190+
cache[b"k"] = same_size_bytes
1191+
new_mtime_ns = path.stat().st_mtime_ns
1192+
1193+
_touch_atime(path, stale_stat)
1194+
# The new file's mtime must be untouched.
1195+
assert path.stat().st_mtime_ns == new_mtime_ns
1196+
1197+
# With a stat that matches the current file, atime is updated and
1198+
# mtime is preserved.
1199+
fresh_stat = path.stat()
1200+
_touch_atime(path, fresh_stat)
1201+
after = path.stat()
1202+
assert after.st_mtime_ns == fresh_stat.st_mtime_ns
1203+
assert after.st_atime_ns >= fresh_stat.st_atime_ns
1204+
1205+
11721206
def test_filestream_cache_returns_bytes_verbatim(tmp_path):
11731207
"""The cache stores raw binary and does no payload validation: whatever
11741208
bytes were written are returned exactly. External tools (cuobjdump,
@@ -1242,13 +1276,22 @@ def test_filestream_cache_rejects_negative_size_cap(tmp_path):
12421276
def test_default_cache_dir_lives_under_user_cache_root():
12431277
"""We delegate per-platform path resolution to ``platformdirs``, so we
12441278
only verify the cuda-python-owned suffix; the OS-specific bits are
1245-
platformdirs' responsibility."""
1279+
platformdirs' responsibility.
1280+
1281+
``opinion=False`` matters: with the default ``opinion=True`` on
1282+
Windows, platformdirs inserts an extra ``Cache`` component that
1283+
would diverge from the documented ``<root>/cuda-python/program-cache``
1284+
layout. Asserting against the same flag pins the layout invariant.
1285+
"""
12461286
import platformdirs
12471287

12481288
from cuda.core.utils._program_cache import _default_cache_dir
12491289

1250-
expected = platformdirs.user_cache_path("cuda-python", appauthor=False) / "program-cache"
1290+
expected = platformdirs.user_cache_path("cuda-python", appauthor=False, opinion=False) / "program-cache"
12511291
assert _default_cache_dir() == expected
1292+
# Path must end with cuda-python/program-cache regardless of platform;
1293+
# no extra "Cache" component sneaks in.
1294+
assert _default_cache_dir().parts[-2:] == ("cuda-python", "program-cache")
12521295

12531296

12541297
def test_filestream_cache_uses_default_dir_when_path_omitted(tmp_path, monkeypatch):

0 commit comments

Comments
 (0)