Skip every write for a reference the reset cannot classify

A reference whose path is under no configured root is meant to be left alone
entirely, but needs_verify was decided before the classification guard, so it
was the one write of four that did not skip. Classification now comes first,
which also stops an out-of-view row being counted as changed or unchanged when
the reset did nothing with it either way.

Stamping the version was also the only database access in the runner that could
throw into its caller, so a transient lock while stamping aborted the seeder's
whole scan -- prune, fast phase and enrichment -- over a step whose own writes
were already committed. A failed stamp is now logged and leaves the step
pending, like every other failure there.
This commit is contained in:
Simon Pinfold
2026-08-18 12:11:09 -07:00
parent 0e6145904a
commit 4f0622a276
3 changed files with 59 additions and 11 deletions

View File

@@ -101,9 +101,18 @@ def run_pending_semantics_steps(interrupt_check: InterruptCheck | None = None) -
)
return applied
with create_session() as session:
set_semantics_version(session, step.version)
session.commit()
try:
with create_session() as session:
set_semantics_version(session, step.version)
session.commit()
except Exception:
logging.exception(
"Asset semantics step %d (%s) finished but could not be stamped, "
"so it runs again; its own writes are already committed",
step.version,
step.description,
)
return applied
applied += 1
logging.info(

View File

@@ -129,14 +129,6 @@ def _reproject_batch(
summary.absent_files += 1
continue
if unchanged:
summary.unchanged_files += 1
else:
# Hand it to the verify path rather than re-read the file for hash and size.
summary.changed_files += 1
if not row.needs_verify:
set_needs_verify.append(row.reference_id)
try:
derived_tags = set(
normalize_tags(get_path_derived_tags_from_path(row.file_path))
@@ -145,6 +137,14 @@ def _reproject_batch(
summary.unclassified_paths += 1
continue
if unchanged:
summary.unchanged_files += 1
else:
# Hand it to the verify path rather than re-read the file for hash and size.
summary.changed_files += 1
if not row.needs_verify:
set_needs_verify.append(row.reference_id)
loader_path = compute_loader_path(row.file_path)
if loader_path != row.loader_path:
loader_paths[row.reference_id] = loader_path

View File

@@ -427,6 +427,24 @@ class TestFileState:
"a file that is simply gone is the scanner's business, not unfinished work"
)
def test_unclassified_row_is_not_flagged_for_verify(self, session, comfy_dirs):
"""needs_verify is a write like any other; it must skip out-of-view rows too."""
_register(
session,
_write(comfy_dirs["elsewhere"], "model.safetensors"),
"ref-1",
mtime_ns=1,
)
summary = reproject_derived_state()
session.expire_all()
assert session.get(AssetReference, "ref-1").needs_verify is False, (
"the one write that did not skip made 'leaves it alone' untrue"
)
assert summary.unclassified_paths == 1
assert summary.changed_files == 0
def test_path_outside_every_known_root_is_left_alone(self, session, comfy_dirs):
path = _write(comfy_dirs["elsewhere"], "model.safetensors")
_register(
@@ -623,6 +641,27 @@ class TestRunner:
):
assert run_pending_semantics_steps() == 0
def test_stamp_failure_does_not_escape_to_the_caller(self, session, comfy_dirs):
"""The seeder calls this; a transient lock here must not abort its scan."""
_register(
session,
_write(comfy_dirs["checkpoints"], "model.safetensors"),
"ref-1",
loader_path=None,
)
with patch(
"app.assets.semantics.set_semantics_version",
side_effect=RuntimeError("database is locked"),
):
assert run_pending_semantics_steps() == 0
session.expire_all()
assert get_semantics_version(session) == 0
assert (
session.get(AssetReference, "ref-1").loader_path == "model.safetensors"
), "the step's own committed work survives a failed stamp"
def test_stamp_is_not_advanced_when_a_step_raises(self, session, comfy_dirs):
def _explode(_interrupt_check):
raise RuntimeError("step failed")