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 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 20:27:08 -04:00
co-authored by Claude Sonnet 5
parent 36d9e7f89b
commit de9c05d0d0
3 changed files with 8 additions and 2 deletions
+1
View File
@@ -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-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 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 2026-08-24 19:00 UTC — Fix video resize handle drifting off-screen
+1 -1
View File
@@ -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
+6 -1
View File
@@ -952,9 +952,14 @@ def _generate_course_thumbnail(course_dir: Path) -> Optional[str]:
best_candidate = None best_candidate = None
best_size = -1 best_size = -1
candidate_paths = [] 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: try:
for i, offset in enumerate(candidate_offsets): 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) candidate_paths.append(candidate_path)
try: try:
result = subprocess.run( result = subprocess.run(