From de9c05d0d05c5e60e6520ee5c9937189b651c70d Mon Sep 17 00:00:00 2001 From: rmsitz Date: Mon, 24 Aug 2026 20:27:08 -0400 Subject: [PATCH] Fix race condition in thumbnail candidate generation Concurrent requests for the same course's thumbnail (e.g. the Precompute background scan overlapping an ordinary page load) shared fixed candidate filenames, so one request's cleanup could delete a temp file the other was still using, crashing with ENOENT. Candidate filenames are now unique per call. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + VERSION | 2 +- offlineu_core.py | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c395c..45425ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +2026-08-25 00:26 UTC — Fix race condition in thumbnail candidate generation 2026-08-25 00:20 UTC — Add autoplay, progress rings, duplicate-lesson detection, title-card thumbnails 2026-08-24 23:22 UTC — Add Favorites, storage drill-down, shortcuts, and What's New 2026-08-24 19:00 UTC — Fix video resize handle drifting off-screen diff --git a/VERSION b/VERSION index 64f37da..541a536 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026-08-25 00:20 UTC — autoplay, progress rings, duplicate lessons, title-card thumbnails +2026-08-25 00:26 UTC — fix race condition in thumbnail candidate generation diff --git a/offlineu_core.py b/offlineu_core.py index d4b5fa3..9ba7704 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -952,9 +952,14 @@ def _generate_course_thumbnail(course_dir: Path) -> Optional[str]: best_candidate = None best_size = -1 candidate_paths = [] + # Unique per call (not just per offset) so two concurrent requests for + # the same course - e.g. the Precompute background scan racing an + # ordinary page load that also misses the cache - never write or clean + # up each other's candidate files out from under one another. + run_id = uuid.uuid4().hex[:8] try: for i, offset in enumerate(candidate_offsets): - candidate_path = course_dir / f'.offlineu_thumbnail_candidate_{i}.jpg' + candidate_path = course_dir / f'.offlineu_thumbnail_candidate_{run_id}_{i}.jpg' candidate_paths.append(candidate_path) try: result = subprocess.run(