From e38840d1df01974bc402a8c87446705c8bee84f2 Mon Sep 17 00:00:00 2001 From: Dima Molodenskiy Date: Tue, 20 Oct 2026 10:17:22 +0200 Subject: [PATCH 1/2] fix: reuse cached scores on nodes with another st_dev file_fingerprint() put the input files' device number into the stat-mode cache identity. A shared filesystem (BeeGFS on the EMBL cluster) reports a different st_dev for the same file on each node, so a recursive run on one node treated every interfaces.csv certified on another as stale and recomputed it, although size, mtime, ctime and inode were identical. In a 1,900-fold AlphaPulldown campaign, 890 of 1,913 manifests differed only in st_dev (65 vs 59), and the summary report spent days rescoring valid tables. Size, mtime_ns, ctime_ns and inode stay in the fingerprint, so edits and replaced files are still detected (the replaced-inputs test is unchanged). Manifests written by 1.4.4 are recomputed once, as after any software change, because the software identity hashes the package source. The new regression test scores a run, then reports another st_dev for every file and checks that the table is reused; it fails on main. Full suite: 183 passed, 6 skipped. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ src/alphajudge/cache.py | 4 +++- test/test_run_integrity.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b88ff..4f790be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Fixed +- Per-run CSV reuse no longer depends on the device number of the input files. A shared filesystem such as BeeGFS reports another `st_dev` for the same file on each cluster node, so a recursive run on one node recomputed every table certified on another. Size, mtime, ctime and inode still detect edits and replaced files. Manifests written by 1.4.4 are recomputed once, as after any software change. + ## 1.4.4 - 2026-10-01 ### Fixed diff --git a/src/alphajudge/cache.py b/src/alphajudge/cache.py index 0622065..43b73de 100644 --- a/src/alphajudge/cache.py +++ b/src/alphajudge/cache.py @@ -49,10 +49,12 @@ def file_fingerprint(path: Path) -> dict[str, int]: ctime supplements mtime, but coarse/preserved timestamps can still hide same-size rewrites. Use content validation when metadata is insufficient. + The device number is not part of it: a shared filesystem (BeeGFS) reports + another st_dev for the same file on each cluster node. """ stat = path.stat() return {"size": stat.st_size, "mtime_ns": stat.st_mtime_ns, "ctime_ns": stat.st_ctime_ns, - "device": stat.st_dev, "inode": stat.st_ino} + "inode": stat.st_ino} def software_identity() -> dict: diff --git a/test/test_run_integrity.py b/test/test_run_integrity.py index 0a80d64..6e85294 100644 --- a/test/test_run_integrity.py +++ b/test/test_run_integrity.py @@ -9,6 +9,7 @@ import shutil from dataclasses import replace from pathlib import Path +from types import SimpleNamespace import numpy as np import pytest @@ -232,6 +233,21 @@ def test_cache_detects_added_removed_and_replaced_inputs(af2_run): assert cache.request_identity(directory, csv_path) != before +def test_cache_is_reused_on_a_node_with_another_device_number(af2_run, record_process, monkeypatch): + directory, _ = af2_run + score(directory) + calls = record_process() + stat = Path.stat + def on_another_node(self, **kwargs): + fields = {name: getattr(stat(self, **kwargs), name) + for name in dir(os.stat_result) if name.startswith("st_")} + fields["st_dev"] += 1 + return SimpleNamespace(**fields) + monkeypatch.setattr(Path, "stat", on_another_node) + score(directory) + assert calls == [] + + def test_png_toggle_generates_missing_plot_without_rescoring(af2_run, monkeypatch): directory, models = af2_run original = score(directory) From 5128e042476e2bb3e9f92bb432c7d75d45b6221b Mon Sep 17 00:00:00 2001 From: Dima Molodenskiy Date: Tue, 20 Oct 2026 12:19:40 +0200 Subject: [PATCH 2/2] test: plots are not redrawn on a node with another st_dev PAE plot fingerprints use the same file_fingerprint(), so a recursive run on another node also redrew every plot it found: the AlphaPulldown campaign's summary report redrew ~10,000 plots before the device number was dropped. The regression test now draws plots on the first run and fails if the second run, with another st_dev, reloads a model to redraw one; it fails on main. The CHANGELOG entry mentions plots. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- test/test_run_integrity.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f790be..6ce1b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased ### Fixed -- Per-run CSV reuse no longer depends on the device number of the input files. A shared filesystem such as BeeGFS reports another `st_dev` for the same file on each cluster node, so a recursive run on one node recomputed every table certified on another. Size, mtime, ctime and inode still detect edits and replaced files. Manifests written by 1.4.4 are recomputed once, as after any software change. +- Per-run CSV and PAE plot reuse no longer depend on the device number of the files. A shared filesystem such as BeeGFS reports another `st_dev` for the same file on each cluster node, so a recursive run on one node recomputed every table and redrew every plot certified on another. Size, mtime, ctime and inode still detect edits and replaced files. Manifests written by 1.4.4 are recomputed once, as after any software change. ## 1.4.4 - 2026-10-01 diff --git a/test/test_run_integrity.py b/test/test_run_integrity.py index 6e85294..fc7c879 100644 --- a/test/test_run_integrity.py +++ b/test/test_run_integrity.py @@ -233,9 +233,9 @@ def test_cache_detects_added_removed_and_replaced_inputs(af2_run): assert cache.request_identity(directory, csv_path) != before -def test_cache_is_reused_on_a_node_with_another_device_number(af2_run, record_process, monkeypatch): +def test_cache_and_plots_are_reused_on_a_node_with_another_device_number(af2_run, record_process, monkeypatch): directory, _ = af2_run - score(directory) + score(directory, skip_pae_png=False) calls = record_process() stat = Path.stat def on_another_node(self, **kwargs): @@ -243,8 +243,11 @@ def on_another_node(self, **kwargs): for name in dir(os.stat_result) if name.startswith("st_")} fields["st_dev"] += 1 return SimpleNamespace(**fields) + def redraw(*args, **kwargs): + pytest.fail("A current PAE plot was redrawn") monkeypatch.setattr(Path, "stat", on_another_node) - score(directory) + monkeypatch.setattr(AF2Parser, "parse_run", redraw) + score(directory, skip_pae_png=False) assert calls == []