From 78162e7022023132aac3ca0ffdef8191c5c12a38 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:54:35 +0200 Subject: [PATCH] fix: write the database cache atomically --- scripts/generate_db.py | 12 ++++++++---- tests/test_audit_regressions.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/scripts/generate_db.py b/scripts/generate_db.py index 2bb1a663..9d4d10d2 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -29,6 +29,7 @@ from common import ( require_yaml, source_yaml_paths, write_if_changed, + write_text_atomic, yaml_load, ) @@ -258,11 +259,14 @@ def load_cache(cache_path: str) -> dict: return {} -def save_cache(cache_path: str, cache: dict): - """Save cache to disk.""" +def save_cache(cache_path: str, cache: dict) -> None: + """Save the cache whole or not at all. + + Written in place, a crash while serializing left a truncated file that + the next run read as empty and rehashed the collection from scratch. + """ os.makedirs(os.path.dirname(cache_path), exist_ok=True) - with open(cache_path, "w") as f: - json.dump(cache, f) + write_text_atomic(cache_path, json.dumps(cache)) def _load_gitignored_large_files() -> set[str]: diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 4e092800..1e97802c 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2161,6 +2161,26 @@ class ContributorsSurviveAFailedRequest(unittest.TestCase): ) +class TheDatabaseCacheIsWrittenWhole(unittest.TestCase): + """A crash while serializing left a truncated db_cache.json behind, and + the next run rehashed the whole collection from scratch.""" + + def test_a_failed_save_keeps_the_previous_cache(self): + import json + from unittest import mock + + from scripts import generate_db + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + path = os.path.join(directory, "cache", "db_cache.json") + generate_db.save_cache(path, {"a": 1}) + with mock.patch.object(json, "dumps", side_effect=TypeError("miscomputed")): + with self.assertRaises(TypeError): + generate_db.save_cache(path, {"a": 2}) + self.assertEqual(generate_db.load_cache(path), {"a": 1}) + self.assertEqual(os.listdir(os.path.dirname(path)), ["db_cache.json"]) + + class KidVidTapesStayWithTheContent(unittest.TestCase): """Stella opens its KidVid tapes by bare name (stella2014, from the working directory) or from its base directory (stella2023, the content