mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-19 12:06:08 +00:00
fix(cache): the atexit stat-index flush must not resurrect a deleted corpus (#2974)
The atexit stat-index flush did an unconditional mkdir(parents=True) on the cache path, so if the corpus/output tree was deleted during the run the flush recreated the whole graphify-out/cache/ chain as a husk. Skip the write when the index root no longer exists (clearing the dirty flag so nothing retries); a live run still writes, and a cold start is unaffected because the root exists before graphify-out/ does.
This commit is contained in:
committed by
safishamsi
parent
384e71e008
commit
c60ddf7b3e
@@ -379,6 +379,20 @@ def _flush_stat_index() -> None:
|
||||
continue
|
||||
dk = _stat_key_to_relative(k, _stat_index_anchor) if _stat_index_anchor is not None else k
|
||||
on_disk[dk] = v
|
||||
# Never resurrect a corpus that was deleted while graphify was running
|
||||
# (#2974): a hook-launched `graphify update . &` in a short-lived worktree
|
||||
# outlives `git worktree remove`, and an unconditional `mkdir -p` here
|
||||
# rebuilt the dead path as a husk holding nothing but this index. The
|
||||
# index is a pure optimisation, so when its root is gone it is simply not
|
||||
# written. Creating graphify-out/cache/ under a root that still exists is
|
||||
# unchanged (a first run writes the index before anything else does).
|
||||
try:
|
||||
if not _stat_index_root.is_dir():
|
||||
_stat_index_dirty = False
|
||||
return
|
||||
except OSError:
|
||||
_stat_index_dirty = False
|
||||
return
|
||||
try:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=p.parent, prefix="stat-index.", suffix=".tmp")
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""The atexit stat-index flush must not resurrect a deleted directory (#2974).
|
||||
|
||||
A post-commit hook runs `graphify update . &` in a short-lived worktree; the
|
||||
branch merges and `git worktree remove` deletes the tree while the rebuild
|
||||
is still running. The exit-time flush then `mkdir -p`'d the dead path back
|
||||
into existence, leaving a husk holding nothing but
|
||||
`graphify-out/cache/stat-index.json` — 81 of them over a few weeks.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from graphify import cache
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _fresh_index():
|
||||
def reset():
|
||||
cache._stat_index_root = None
|
||||
cache._stat_index_anchor = None
|
||||
cache._stat_index = {}
|
||||
cache._stat_index_dirty = False
|
||||
reset()
|
||||
yield
|
||||
reset()
|
||||
|
||||
|
||||
def _dirty(corpus: Path) -> Path:
|
||||
f = corpus / "a.md"
|
||||
f.write_text("# hello\nbody\n", encoding="utf-8")
|
||||
cache.file_hash(f, corpus) # loads + dirties the index for this corpus
|
||||
assert cache._stat_index_dirty
|
||||
return f
|
||||
|
||||
|
||||
def test_a_corpus_deleted_mid_run_stays_deleted(tmp_path):
|
||||
corpus = tmp_path / "husk-race-corpus"
|
||||
corpus.mkdir()
|
||||
_dirty(corpus)
|
||||
shutil.rmtree(corpus)
|
||||
cache._flush_stat_index() # what atexit does
|
||||
assert not corpus.exists(), "the flush resurrected the deleted corpus"
|
||||
assert not cache._stat_index_dirty # nothing left pending for a second attempt
|
||||
|
||||
|
||||
def test_a_redirected_cache_root_that_vanished_is_not_recreated(tmp_path):
|
||||
corpus = tmp_path / "c"
|
||||
corpus.mkdir()
|
||||
elsewhere = tmp_path / "out"
|
||||
elsewhere.mkdir()
|
||||
f = corpus / "a.md"
|
||||
f.write_text("x\n", encoding="utf-8")
|
||||
cache.file_hash(f, corpus, cache_root=elsewhere)
|
||||
shutil.rmtree(elsewhere)
|
||||
cache._flush_stat_index()
|
||||
assert not elsewhere.exists()
|
||||
|
||||
|
||||
def test_the_index_is_still_written_for_a_live_run(tmp_path):
|
||||
"""A first run writes the index before graphify-out/ exists at all; that
|
||||
stays as it was — the root is live, so creating cache/ under it is fine."""
|
||||
corpus = tmp_path / "c"
|
||||
corpus.mkdir()
|
||||
_dirty(corpus)
|
||||
cache._flush_stat_index()
|
||||
assert (corpus / "graphify-out" / "cache" / "stat-index.json").is_file()
|
||||
Reference in New Issue
Block a user