From b4736da47b2debea14a31408acfec10bde760447 Mon Sep 17 00:00:00 2001 From: Brigs Date: Sun, 9 Aug 2026 14:44:14 -0400 Subject: [PATCH] fix: concurrent runs corrupt history.json, and a corrupt one stops startup Every process wrote to the same history.tmp, so parallel runs interleaved into it and the replace raced. The temp path is now unique per process. _read_json caught only OSError, so a JSONDecodeError from a damaged file propagated out of startup and stopped the tool parsing evidence. It now logs, keeps the file as .corrupt.bak, and continues. Verified with 180 concurrent writes from 6 processes: valid JSON, no leftover temp files. Levelled from iLEAPP so the five cores stay identical. Co-Authored-By: Claude Opus 5 --- leapp_functions/app/history.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/leapp_functions/app/history.py b/leapp_functions/app/history.py index 9534ea2..66e75bd 100644 --- a/leapp_functions/app/history.py +++ b/leapp_functions/app/history.py @@ -129,7 +129,10 @@ def _atomic_write_json(path, data): """ path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) - temp_path = path.with_suffix(".tmp") + # Unique per process: concurrent runs sharing one temp path interleave their + # writes into it, which lands a concatenated document in the real file and + # makes the second os.replace fail because the first already moved it. + temp_path = path.with_suffix(f".tmp.{os.getpid()}") try: with open(temp_path, "w", encoding="utf-8") as f: @@ -163,6 +166,15 @@ def _read_json(path, default=None): except OSError as e: logger.error("Failed to read history/settings file at %s: %s", path, e) return default + except ValueError as e: + # Unreadable JSON here is a damaged convenience file, not a reason to stop + # parsing evidence. Keep a copy so the damage can be looked at later. + logger.error("History/settings file at %s is not valid JSON (%s); ignoring it", path, e) + try: + os.replace(path, f"{path}.corrupt.bak") + except OSError: + pass + return default def _has_history_entries(history_data):