lazy boot: only repopulate when the index references a missing source

_writable_store_index_needs_population compared the set of stored row
hashes against the set of index-referenced hashes symmetrically. The
store is append-only (only reconcile_index prunes), so rows orphaned by
a content change, an ad-hoc self-heal superseded by conf context, or an
uninstall accumulate legitimately - and one orphan made the comparison
unequal forever, turning every boot and every post-install reload into a
full inline repopulate and permanently defeating the freshness-token
scan skip (observed: 5 inline populates across one shed-install
integration test, 3 orphaned rows as the stable delta).

Check the direction that is actually an invariant: every source_hash the
index references must resolve to a stored row, or materialising that
tool would fail. Orphaned rows are logged at debug as prune candidates.
Measured on the same test: 5 inline populates -> 2 (cold boot + the
legitimate freshness-token change from the install itself).
This commit is contained in:
mvdbeek
2026-07-28 17:27:46 +02:00
parent e38aca1d96
commit a63f88a421
2 changed files with 45 additions and 3 deletions
+24 -3
View File
@@ -826,6 +826,17 @@ class LazyToolBox(ToolBox):
return False
def _writable_store_index_needs_population(self) -> bool:
"""Return True when an index entry references a source the store lost.
The invariant that matters is one-directional: every ``source_hash``
the index references must resolve to a stored row, or materialising
that tool would fail. The reverse is not staleness — the store is
append-only (only ``reconcile_index`` prunes), so rows orphaned by a
content change, an ad-hoc self-heal superseded by conf context, or an
uninstall accumulate legitimately. A symmetric comparison here turned
one such orphan into a full inline repopulate on every boot and
reload, permanently defeating the freshness-token scan skip.
"""
if self._store is None:
return False
stores = (
@@ -841,15 +852,25 @@ class LazyToolBox(ToolBox):
index_hashes.update(entry.source_hash for entry in index.entries.values() if entry.source_hash)
for versions in index.entries_by_version.values():
index_hashes.update(entry.source_hash for entry in versions.values() if entry.source_hash)
if source_hashes != index_hashes:
dangling = index_hashes - source_hashes
if dangling:
log.info(
"Tool source store index/source mismatch for %s (%d indexed sources, %d stored sources); "
"running populator",
"Tool source store index for %s references %d source(s) missing from the store "
"(%d indexed, %d stored); running populator",
store_name,
len(dangling),
len(index_hashes),
len(source_hashes),
)
return True
orphaned = len(source_hashes - index_hashes)
if orphaned:
log.debug(
"Tool source store %s holds %d source row(s) no index entry references "
"(superseded content or ad-hoc rows; reconcile_index prunes them)",
store_name,
orphaned,
)
return False
def _run_inline_populator(self) -> None:
+21
View File
@@ -333,6 +333,27 @@ def test_resolve_index_entry_returns_none_when_nothing_matches():
assert box._resolve_index_entry(None, None) is None
def _reconcile_box(index_hashes, store_hashes):
box = _seam_box()
box._store.read_only = False
box._store.list_all.return_value = list(store_hashes)
index = ToolIndex()
for position, source_hash in enumerate(index_hashes):
index.add_entry(_entry(id=f"tool_{position}", source_hash=source_hash))
box._store.load_index.return_value = index
return box
def test_index_reconcile_ignores_orphaned_store_rows():
box = _reconcile_box(index_hashes=["a", "b"], store_hashes=["a", "b", "orphaned"])
assert box._writable_store_index_needs_population() is False
def test_index_reconcile_repopulates_on_dangling_index_reference():
box = _reconcile_box(index_hashes=["a", "b"], store_hashes=["a"])
assert box._writable_store_index_needs_population() is True
def test_create_tool_populates_adhoc_for_existing_file(tmp_path, monkeypatch):
# Shed installs load cloned tools during metadata generation, before
# any conf is persisted — a miss for an on-disk file populates that