From d11289309b285724f820b0d0e4290fcbfef5ec67 Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Sat, 16 May 2026 22:51:32 +0900 Subject: [PATCH] v0.4.19: fix post-move publish race in VideoCache.download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer-flagged: v0.4.18 only checked epoch BEFORE Files.move. The window between the move completing and READY.put / "완료" chat was still racy — if /videoCache clear landed in that window, clearAll would epoch++ + clear READY + delete files on disk, then the download thread would do READY.put(url, finalPath) anyway, resurrecting a cleared entry and emitting a stale "완료" message. Add a second epoch check immediately AFTER Files.move(): if the epoch changed, delete finalPath and return without publishing. The pre-move check is kept too — it lets the common cancel-during-read case skip the wasted move/delete round-trip. --- gradle.properties | 2 +- .../client/playback/VideoCache.java | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf86871..660711a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.configuration-cache=false # Mod mod_id=video_player -mod_version=0.4.18 +mod_version=0.4.19 maven_group=com.ejclaw.videoplayer archives_base_name=video_player diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java index 8df22b0..87a902a 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java +++ b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java @@ -340,11 +340,10 @@ public final class VideoCache { return; } - // Final cancellation check before publishing. If clearAll ran during the read - // loop's last iteration (after the loop check, before this point), we still - // refuse to publish — otherwise we'd resurrect a cache entry post-clear. + // Pre-move cancellation check. If clearAll ran during the read loop, abort + // before promoting .part to final — saves a wasted move + delete. if (CACHE_EPOCH.get() != startEpoch) { - VideoPlayerMod.LOG.info("[{}] preload: cancelled at publish (clearAll ran) — {}", + VideoPlayerMod.LOG.info("[{}] preload: cancelled before move (clearAll ran) — {}", VideoPlayerMod.MOD_ID, url); try { Files.deleteIfExists(partPath); } catch (Throwable ignored) {} return; @@ -352,6 +351,21 @@ public final class VideoCache { Files.move(partPath, finalPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + + // Post-move cancellation check. clearAll() may have run between the pre-move + // check above and the move itself — in that case clearAll's directory scan + // either missed our file (it didn't exist yet) or saw the .part and skipped / + // failed to delete it. Either way, finalPath now exists on disk but the + // user-visible cache state is "cleared", so we must delete the file and skip + // both the READY.put and the "완료" chat. Without this check, a clear right at + // this window leaves a resurrected file in READY and a stale "완료" message. + if (CACHE_EPOCH.get() != startEpoch) { + VideoPlayerMod.LOG.info("[{}] preload: cancelled after move (clearAll ran) — {}", + VideoPlayerMod.MOD_ID, url); + try { Files.deleteIfExists(finalPath); } catch (Throwable ignored) {} + return; + } + READY.put(url, finalPath); VideoPlayerMod.LOG.info("[{}] preload: cached {} ({} bytes) -> {}", VideoPlayerMod.MOD_ID, url, total, finalPath.getFileName());