fix: atomic writes for manifest/record_run/update_best (tmp+replace)
This commit is contained in:
@@ -64,6 +64,21 @@ def _now_iso() -> str:
|
|||||||
return datetime.now(UTC).isoformat()
|
return datetime.now(UTC).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write_json(path: Path, data: dict) -> None:
|
||||||
|
"""原子写 JSON:tmp + os.replace(对齐 checkpoint.py 范式,防半截损坏)。
|
||||||
|
|
||||||
|
先写同目录临时文件,再 os.replace 原子替换目标;替换阶段崩溃不会留下半截
|
||||||
|
JSON,原文件保持完好。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
path: 目标 JSON 文件路径。
|
||||||
|
data: 待序列化的字典。
|
||||||
|
"""
|
||||||
|
tmp = path.with_name(path.name + ".tmp")
|
||||||
|
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Workspace 核心函数
|
# Workspace 核心函数
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -108,7 +123,7 @@ def _scaffold_workspace(
|
|||||||
},
|
},
|
||||||
"history": [],
|
"history": [],
|
||||||
}
|
}
|
||||||
(workspace_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2))
|
_atomic_write_json(workspace_dir / "manifest.json", manifest)
|
||||||
|
|
||||||
|
|
||||||
def init_workspace(
|
def init_workspace(
|
||||||
@@ -286,7 +301,7 @@ def update_manifest(workspace_dir: Path, **version_updates: str) -> None:
|
|||||||
raise KeyError(f"无效的 manifest current 字段: {invalid}")
|
raise KeyError(f"无效的 manifest current 字段: {invalid}")
|
||||||
manifest = load_manifest(workspace_dir)
|
manifest = load_manifest(workspace_dir)
|
||||||
manifest["current"].update(version_updates)
|
manifest["current"].update(version_updates)
|
||||||
(workspace_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2))
|
_atomic_write_json(workspace_dir / "manifest.json", manifest)
|
||||||
|
|
||||||
|
|
||||||
def record_run(workspace_dir: Path, run_id: str) -> Path:
|
def record_run(workspace_dir: Path, run_id: str) -> Path:
|
||||||
@@ -315,9 +330,7 @@ def record_run(workspace_dir: Path, run_id: str) -> Path:
|
|||||||
"questions": current["questions"],
|
"questions": current["questions"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(workspace_dir / "manifest.json").write_text(
|
_atomic_write_json(workspace_dir / "manifest.json", manifest)
|
||||||
json.dumps(manifest, ensure_ascii=False, indent=2)
|
|
||||||
)
|
|
||||||
|
|
||||||
run_dir = workspace_dir / "runs" / run_id
|
run_dir = workspace_dir / "runs" / run_id
|
||||||
# exist_ok:同 run_id 重跑时 run 目录已存在不应崩溃
|
# exist_ok:同 run_id 重跑时 run 目录已存在不应崩溃
|
||||||
@@ -369,7 +382,7 @@ def update_best(
|
|||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"epoch": epoch,
|
"epoch": epoch,
|
||||||
}
|
}
|
||||||
(workspace_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2))
|
_atomic_write_json(workspace_dir / "manifest.json", manifest)
|
||||||
logger.info("Best 已更新: val_acc={}, run={}, epoch={}", val_acc, run_id, epoch)
|
logger.info("Best 已更新: val_acc={}, run={}, epoch={}", val_acc, run_id, epoch)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,33 @@ def test_update_manifest_valid(store_dir: Path, workspace_dir: Path) -> None:
|
|||||||
assert manifest["current"]["skills"] == "skills/v2"
|
assert manifest["current"]["skills"] == "skills/v2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_manifest_is_atomic(
|
||||||
|
store_dir: Path, workspace_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""写 manifest 途中崩溃不产生半截 JSON(原子写:os.replace 失败也不损原文件)。"""
|
||||||
|
from app.harness import workspace as ws
|
||||||
|
|
||||||
|
init_workspace(
|
||||||
|
workspace_dir,
|
||||||
|
store_dir,
|
||||||
|
questions="benchmarks/Video-MME",
|
||||||
|
skills_version="v1",
|
||||||
|
prompts_version="v1",
|
||||||
|
)
|
||||||
|
original = (workspace_dir / "manifest.json").read_text()
|
||||||
|
|
||||||
|
def _boom(_src: object, _dst: object) -> None:
|
||||||
|
raise OSError("crash during replace")
|
||||||
|
|
||||||
|
monkeypatch.setattr(ws.os, "replace", _boom)
|
||||||
|
with pytest.raises(OSError, match="crash during replace"):
|
||||||
|
update_manifest(workspace_dir, skills="skills/v2")
|
||||||
|
|
||||||
|
# 原 manifest 未被破坏(内容不变且仍是合法 JSON)
|
||||||
|
assert (workspace_dir / "manifest.json").read_text() == original
|
||||||
|
assert json.loads((workspace_dir / "manifest.json").read_text())
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# record_run
|
# record_run
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user