From 10c5939bcbe6d5ddf1dd317f4370319c6e39daed Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sun, 9 Aug 2026 03:39:35 +0300 Subject: [PATCH] bgp: clamp cache age to zero against windows mtime clock skew time.time() - path.stat().st_mtime went negative under load when NTFS stamped a write's mtime slightly ahead of the wall clock read a moment later, making a ttl_hours=0 cache lookup serve stale data instead of refetching. surfaced as a flaky failure in test_cached_json_refetches_once_the_entry_expires during the full suite run before task 40's smoke test. --- src/netcheck/bgp.py | 3 ++- tests/test_bgp.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/netcheck/bgp.py b/src/netcheck/bgp.py index 745f3e3..7b71302 100644 --- a/src/netcheck/bgp.py +++ b/src/netcheck/bgp.py @@ -160,7 +160,8 @@ async def cached_json( fetch: Callable[[], Awaitable[dict]], ) -> dict: path = Path(cache_dir) / f"{key}.json" - if path.exists() and (time.time() - path.stat().st_mtime) < ttl_hours * 3600: + age = max(0.0, time.time() - path.stat().st_mtime) if path.exists() else None + if age is not None and age < ttl_hours * 3600: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError): diff --git a/tests/test_bgp.py b/tests/test_bgp.py index 8038678..362e95e 100644 --- a/tests/test_bgp.py +++ b/tests/test_bgp.py @@ -185,6 +185,23 @@ async def test_cached_json_refetches_once_the_entry_expires(tmp_path: Path): assert len(calls) == 2 +async def test_cached_json_refetches_when_mtime_reads_ahead_of_now(tmp_path: Path): + import os + + calls = [] + + async def fetch() -> dict: + calls.append(1) + return {"value": len(calls)} + + await cached_json(tmp_path, "k", ttl_hours=24, fetch=fetch) + future = (tmp_path / "k.json").stat().st_mtime + 5 + os.utime(tmp_path / "k.json", (future, future)) + result = await cached_json(tmp_path, "k", ttl_hours=0, fetch=fetch) + assert result == {"value": 2} + assert len(calls) == 2 + + async def test_cached_json_survives_a_corrupt_cache_file(tmp_path: Path): (tmp_path / "k.json").write_text("{not json", encoding="utf-8")