Fix: Start-Konflikt-Waechter friert nach Update nicht mehr die ganze Liste ein
Nach einem Auto-Update-Neustart blieb die komplette Warteschlange stehen
(running=false, hunderte Items wartend), obwohl Auto-Resume aktiv war.
Ursache war der Start-Konflikt-Waechter:
- getStartConflicts() meldete jedes Paket als Konflikt, dessen
paketspezifischer Entpack-Ordner bereits Dateien enthielt. Ein nur
teilweise heruntergeladenes Paket hat dort aber seine EIGENEN fertigen
Teile liegen (z.B. 15 von 26 fertig und entpackt) - das ist kein
Konflikt, sondern Wiederaufnahme.
- Der Auto-Resume-Waechter war binaer: ein einziger gemeldeter Konflikt
hat ALLE Pakete blockiert ("Auto-Resume uebersprungen: Start-Konflikte
erkannt"). Zwei teilfertige Pakete haben so 30 Pakete eingefroren.
Fix:
- Diskriminator in getStartConflicts(): ein Paket mit eigenen fertigen
("completed") Items zaehlt nicht mehr als Konflikt - die Dateien im
Entpack-Ordner sind seine eigene Ausgabe, kein Fremdbestand.
- Selektives Auto-Resume: start() akzeptiert excludePackageIds und laesst
diese Pakete aus dem Lauf-Set (runPackageIds, auf das der Scheduler
filtert). beginAutoResume() startet die sauberen Pakete und haelt nur
echte Konflikte zurueck, statt alles zu blockieren; die zurueckgehaltenen
Pakete werden im Log namentlich genannt.
Die beiden Aenderungen greifen ineinander: der Diskriminator garantiert,
dass ein als Konflikt gemeldetes Paket keine fertigen Teile hat, also auch
keine ausstehende Nachbearbeitung - das defensive runPackageIds.delete kann
daher keine Entpack-Arbeit haengen lassen.
Tests: Diskriminator (eigene Ausgabe nicht geflaggt / Fremdbestand
geflaggt) plus selektives Resume (ausgeschlossenes Paket bleibt aus dem
Lauf-Set, laeuft nicht an).
This commit is contained in:
parent
d73b446d68
commit
39fbc54818
@ -180,21 +180,13 @@ export class AppController {
|
||||
if (this.settings.autoResumeOnStart) {
|
||||
const snapshot = this.manager.getSnapshot();
|
||||
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
||||
if (hasPending) {
|
||||
void this.manager.getStartConflicts().then((conflicts) => {
|
||||
const hasConflicts = conflicts.length > 0;
|
||||
if (this.hasAnyProviderToken(this.settings) && !hasConflicts) {
|
||||
if (hasPending && this.hasAnyProviderToken(this.settings)) {
|
||||
if (this.onStateHandler) {
|
||||
logger.info("Auto-Resume beim Start aktiviert (nach Konflikt-Check)");
|
||||
void this.manager.start().catch((err) => logger.warn(`Auto-Resume Start Fehler: ${String(err)}`));
|
||||
this.beginAutoResume();
|
||||
} else {
|
||||
this.autoResumePending = true;
|
||||
logger.info("Auto-Resume beim Start vorgemerkt");
|
||||
}
|
||||
} else if (hasConflicts) {
|
||||
logger.info("Auto-Resume übersprungen: Start-Konflikte erkannt");
|
||||
}
|
||||
}).catch((err) => logger.warn(`getStartConflicts Fehler (constructor): ${String(err)}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -251,14 +243,27 @@ export class AppController {
|
||||
handler(this.manager.getSnapshot());
|
||||
if (this.autoResumePending) {
|
||||
this.autoResumePending = false;
|
||||
void this.manager.start().catch((err) => logger.warn(`Auto-Resume Start Fehler: ${String(err)}`));
|
||||
logger.info("Auto-Resume beim Start aktiviert");
|
||||
this.beginAutoResume();
|
||||
} else {
|
||||
this.manager.triggerIdleExtractions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private beginAutoResume(): void {
|
||||
void this.manager.getStartConflicts().then((conflicts) => {
|
||||
const excludePackageIds = new Set(conflicts.map((conflict) => conflict.packageId));
|
||||
if (excludePackageIds.size > 0) {
|
||||
const names = conflicts.map((conflict) => conflict.packageName).join(", ");
|
||||
logger.info(`Auto-Resume: ${excludePackageIds.size} Paket(e) mit Start-Konflikt zurückgehalten (${names}); übrige Pakete starten`);
|
||||
} else {
|
||||
logger.info("Auto-Resume beim Start aktiviert (keine Start-Konflikte)");
|
||||
}
|
||||
void this.manager.start(excludePackageIds.size > 0 ? { excludePackageIds } : undefined)
|
||||
.catch((err) => logger.warn(`Auto-Resume Start Fehler: ${String(err)}`));
|
||||
}).catch((err) => logger.warn(`Auto-Resume Konflikt-Check Fehler: ${String(err)}`));
|
||||
}
|
||||
|
||||
public getSnapshot(): UiSnapshot {
|
||||
return this.manager.getSnapshot();
|
||||
}
|
||||
|
||||
@ -3011,6 +3011,14 @@ export class DownloadManager extends EventEmitter {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasOwnCompletedOutput = pkg.itemIds.some((itemId) => {
|
||||
const item = this.session.items[itemId];
|
||||
return Boolean(item && item.status === "completed");
|
||||
});
|
||||
if (hasOwnCompletedOutput) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.isPackageSpecificExtractDir(pkg)) {
|
||||
continue;
|
||||
}
|
||||
@ -5562,7 +5570,7 @@ export class DownloadManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
public async start(options?: { excludePackageIds?: ReadonlySet<string> }): Promise<void> {
|
||||
if (this.session.running) {
|
||||
return;
|
||||
}
|
||||
@ -5603,6 +5611,9 @@ export class DownloadManager extends EventEmitter {
|
||||
if (item.status !== "queued" && item.status !== "reconnect_wait") {
|
||||
return false;
|
||||
}
|
||||
if (options?.excludePackageIds?.has(item.packageId)) {
|
||||
return false;
|
||||
}
|
||||
const pkg = this.session.packages[item.packageId];
|
||||
return Boolean(pkg && !pkg.cancelled && pkg.enabled);
|
||||
});
|
||||
@ -5670,6 +5681,11 @@ export class DownloadManager extends EventEmitter {
|
||||
this.itemContributedBytes.clear();
|
||||
this.reservedTargetPaths.clear();
|
||||
this.claimedTargetPathByItem.clear();
|
||||
if (options?.excludePackageIds) {
|
||||
for (const excluded of options.excludePackageIds) {
|
||||
this.runPackageIds.delete(excluded);
|
||||
}
|
||||
}
|
||||
|
||||
this.session.running = true;
|
||||
this.session.paused = false;
|
||||
|
||||
@ -12244,3 +12244,121 @@ describe("download manager", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("start conflict guard + selective resume", () => {
|
||||
function makeItem(id: string, packageId: string, status: string, fileName: string): any {
|
||||
return {
|
||||
id, packageId, url: `https://hoster.example/${id}`, provider: "realdebrid",
|
||||
status, retries: 0, speedBps: 0, downloadedBytes: status === "completed" ? 100 : 0,
|
||||
totalBytes: status === "completed" ? 100 : null, progressPercent: status === "completed" ? 100 : 0,
|
||||
fileName, targetPath: "", resumable: true, attempts: 0, lastError: "", fullStatus: "",
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
it("does not flag a partially-downloaded package whose extract dir holds its own completed output", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-startconflict-own-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "pkg-own-output";
|
||||
const extractDir = path.join(root, "extract", "OwnOutput");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "episode01.mkv"), Buffer.alloc(64, 7));
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId, name: "OwnOutput",
|
||||
outputDir: path.join(root, "downloads", "OwnOutput"), extractDir,
|
||||
status: "queued", itemIds: ["own-done", "own-pending"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["own-done"] = makeItem("own-done", packageId, "completed", "done.rar");
|
||||
session.items["own-pending"] = makeItem("own-pending", packageId, "queued", "pending.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
|
||||
const conflicts = await manager.getStartConflicts();
|
||||
expect(conflicts.map((c) => c.packageId)).not.toContain(packageId);
|
||||
});
|
||||
|
||||
it("flags a fresh package when its package-specific extract dir already holds files and it has no completed items", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-startconflict-fresh-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "pkg-fresh-conflict";
|
||||
const extractDir = path.join(root, "extract", "FreshConflict");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "old-from-previous-run.mkv"), Buffer.alloc(64, 9));
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId, name: "FreshConflict",
|
||||
outputDir: path.join(root, "downloads", "FreshConflict"), extractDir,
|
||||
status: "queued", itemIds: ["fresh-pending"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["fresh-pending"] = makeItem("fresh-pending", packageId, "queued", "fresh.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
|
||||
const conflicts = await manager.getStartConflicts();
|
||||
expect(conflicts.map((c) => c.packageId)).toContain(packageId);
|
||||
});
|
||||
|
||||
it("start() holds excluded packages out of the run set and runs the rest", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-selective-resume-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const runId = "pkg-run";
|
||||
const holdId = "pkg-hold";
|
||||
fs.mkdirSync(path.join(root, "downloads", "RunMe"), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, "downloads", "HoldMe"), { recursive: true });
|
||||
session.packageOrder = [runId, holdId];
|
||||
session.packages[runId] = {
|
||||
id: runId, name: "RunMe",
|
||||
outputDir: path.join(root, "downloads", "RunMe"), extractDir: path.join(root, "extract", "RunMe"),
|
||||
status: "queued", itemIds: ["run-item"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.packages[holdId] = {
|
||||
id: holdId, name: "HoldMe",
|
||||
outputDir: path.join(root, "downloads", "HoldMe"), extractDir: path.join(root, "extract", "HoldMe"),
|
||||
status: "queued", itemIds: ["hold-item"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["run-item"] = makeItem("run-item", runId, "queued", "run.rar");
|
||||
session.items["hold-item"] = makeItem("hold-item", holdId, "queued", "hold.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", maxParallel: 2, outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
(manager as any).debridService.unrestrictLink = () => new Promise(() => {});
|
||||
|
||||
await manager.start({ excludePackageIds: new Set([holdId]) });
|
||||
|
||||
expect((manager as any).runPackageIds.has(runId)).toBe(true);
|
||||
expect((manager as any).runPackageIds.has(holdId)).toBe(false);
|
||||
expect((manager as any).runItemIds.has("run-item")).toBe(true);
|
||||
expect((manager as any).runItemIds.has("hold-item")).toBe(false);
|
||||
expect(session.items["hold-item"].status).toBe("queued");
|
||||
|
||||
manager.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user