Fix: Hybrid-Rename-Race — 1-2 Dateien pro Staffel blieben unbenannt
User-Report (verifiziert via Support-Bundle): pl3x-24hours.s01e07, tmsf-burnnotice-s05e11-repack, -s05e15 landeten mit Original-Scene-Namen in der Library statt umbenannt. Andere Episoden derselben Pakete (formatidentisch) wurden korrekt umbenannt → kein Format-Problem, sondern Timing-Race. Root Cause (aus Log-Timeline): 1. autoRenameExtractedVideoFilesImpl erfasste `now` EINMAL am Scan-Start. Bei Hybrid-Extraktion werden weitere Dateien WÄHREND des Scans geschrieben → deren mtime > now → negatives ageMs → der "Clock-Skew = stabil"-Zweig wertete sie faelschlich als stabil → Rename mitten im Extractor-Write → EBUSY → 200ms- Retry deferred. 2. Der MKV-Collect hatte KEINEN Frische-Skip und moved die Datei im Retry-Fenster mit Original-Namen, bevor der Rename-Retry feuerte. 3. Rename + Collect liefen als zwei separate chainPackageFileOp-Ketten → ueberlappende Hybrid-Runden konnten einen Collect zwischen Rename und Collect einer anderen Runde einschieben. Fix (3 Teile, scoped auf extractDir des Pakets — kein Shared-Library-Scan, nicht das v1.7.107-Antipattern): 1. `now` wird PRO DATEI erfasst → frisch-geschriebene Dateien korrekt als "frisch" erkannt und deferred (statt EBUSY-Rename mitten im Write). 2. collectMkvFilesToLibrary bekommt deferFreshFiles-Param: im Hybrid-Pfad werden frische Dateien (juenger als fileStabilizeMinAgeMs) uebersprungen statt unbenannt gemoved. Der finale Deferred-Pass (deferFreshFiles=false) sammelt sie nach Stabilisierung ein (Safety-Net). 3. Hybrid-Pfad: Rename (Impl-Variante, kein Self-Chain) + Collect in EINER chainPackageFileOp-Kette → atomar, kein Interleaving ueberlappender Runden. Deferred-Pfad unangetastet (dort keine concurrent Extraktion). Regressionstest: frische Datei wird im Hybrid-Collect deferred, vom finalen Pass gesammelt. 622 Tests gruen, tsc-Fehlerzahl unveraendert (9 pre-existing). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e061997ed2
commit
18eada963f
@@ -3995,7 +3995,6 @@ export class DownloadManager extends EventEmitter {
|
||||
// they've stabilized (hybrid-extract fires a new rename scan after every
|
||||
// archive completes, so nothing gets missed).
|
||||
const FILE_STABILIZE_MIN_AGE_MS = this.fileStabilizeMinAgeMs;
|
||||
const now = Date.now();
|
||||
for (const sourcePath of videoFiles) {
|
||||
if (shouldAbort?.()) {
|
||||
return renamed;
|
||||
@@ -4013,6 +4012,13 @@ export class DownloadManager extends EventEmitter {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
// now PER FILE erfassen (nicht einmal am Scan-Start): bei Hybrid-Extraktion
|
||||
// werden weitere Dateien WÄHREND dieses Scans geschrieben. Ein am Scan-Start
|
||||
// erfasstes now waere fuer solche Dateien aelter als ihre mtime → negatives
|
||||
// ageMs → der Clock-Skew-Zweig unten wuerde sie faelschlich als "stabil"
|
||||
// werten und einen Rename mitten im Extractor-Write ausloesen (EBUSY →
|
||||
// deferred → der Collect moved die Datei mit Original-Namen, statt umbenannt).
|
||||
const now = Date.now();
|
||||
const ageMs = now - sourceStat.mtimeMs;
|
||||
// Negative age = mtime in the future (clock skew, NTP correction,
|
||||
// VM resume after suspension). Treat as "definitely stable" so the
|
||||
@@ -4655,7 +4661,8 @@ export class DownloadManager extends EventEmitter {
|
||||
private async collectMkvFilesToLibrary(
|
||||
packageId: string,
|
||||
pkg: PackageEntry,
|
||||
shouldAbort?: () => boolean
|
||||
shouldAbort?: () => boolean,
|
||||
deferFreshFiles = false
|
||||
): Promise<void> {
|
||||
if (!this.settings.collectMkvToLibrary) {
|
||||
return;
|
||||
@@ -4817,13 +4824,29 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
// Skip 0-byte files from failed/partial extractions
|
||||
let sourceSize = 0;
|
||||
let sourceMtimeMs = 0;
|
||||
try {
|
||||
const stat = await fs.promises.stat(sourcePath);
|
||||
sourceSize = stat.size;
|
||||
sourceMtimeMs = stat.mtimeMs;
|
||||
} catch {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
// Frische-Skip (nur Hybrid-Pfad: deferFreshFiles=true): eine gerade extrahierte
|
||||
// Datei wird vom Auto-Rename absichtlich deferred (noch nicht stabil / EBUSY).
|
||||
// Wuerde der Collect sie JETZT moven, landet sie mit Original-Namen in der
|
||||
// Library statt umbenannt (genau der gemeldete "1-2 pro Staffel nicht
|
||||
// umbenannt"-Bug). Wir defern sie ebenfalls → eine spaetere Hybrid-Runde oder
|
||||
// der finale Deferred-Pass (deferFreshFiles=false) benennt sie um + sammelt sie.
|
||||
if (deferFreshFiles && this.fileStabilizeMinAgeMs > 0) {
|
||||
const ageMs = Date.now() - sourceMtimeMs;
|
||||
if (ageMs >= 0 && ageMs < this.fileStabilizeMinAgeMs) {
|
||||
logger.info(`MKV-Sammelordner: ${path.basename(sourcePath)} uebersprungen — Datei noch frisch (${Math.floor(ageMs)}ms), wird nach Stabilisierung gesammelt`);
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (sourceSize === 0) {
|
||||
logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`);
|
||||
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetDir);
|
||||
@@ -11361,15 +11384,21 @@ export class DownloadManager extends EventEmitter {
|
||||
hybridSet.add(hybridController);
|
||||
const hybridShouldAbort = (): boolean => hybridController.signal.aborted || this.session.packages[packageId] !== pkg;
|
||||
void (async () => {
|
||||
// Atomare Kopplung von Rename + Collect in EINER chainPackageFileOp-Kette,
|
||||
// damit zwischen ihnen keine andere (ueberlappende) Hybrid-Runde ihren
|
||||
// Collect einschieben kann (das war der Rename-Race: ein Collect moved
|
||||
// eine Datei bevor der zugehoerige Rename lief). Wichtig: die IMPL-Variante
|
||||
// des Renames verwenden — die Public-Variante ruft selbst chainPackageFileOp
|
||||
// auf, was hier zu verschachteltem Chaining (Deadlock) fuehren wuerde.
|
||||
// deferFreshFiles=true: Dateien die der Rename als "noch frisch" auslaesst
|
||||
// werden vom Collect ebenfalls deferred (statt mit Original-Namen gemoved).
|
||||
try {
|
||||
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg, hybridShouldAbort);
|
||||
await this.chainPackageFileOp(pkg.id, async () => {
|
||||
await this.autoRenameExtractedVideoFilesImpl(pkg.extractDir, pkg, hybridShouldAbort);
|
||||
await this.collectMkvFilesToLibrary(packageId, pkg, hybridShouldAbort, true);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`Hybrid Auto-Rename Fehler: pkg=${pkg.name}, reason=${compactErrorText(err)}`);
|
||||
}
|
||||
try {
|
||||
await this.chainPackageFileOp(pkg.id, () => this.collectMkvFilesToLibrary(packageId, pkg, hybridShouldAbort));
|
||||
} catch (err) {
|
||||
logger.warn(`Hybrid MKV-Collection Fehler: pkg=${pkg.name}, reason=${compactErrorText(err)}`);
|
||||
logger.warn(`Hybrid Post-Extract (Rename+Collect) Fehler: pkg=${pkg.name}, reason=${compactErrorText(err)}`);
|
||||
} finally {
|
||||
const set = this.packageHybridPostProcessControllers.get(packageId);
|
||||
if (set) {
|
||||
|
||||
Reference in New Issue
Block a user