Fix: festes Retry-Limit wird jetzt hart eingehalten — kein Endlos-Shelve-Loop mehr (Runde-6-Audit)
Bei einem FESTEN retryLimit (>=5) sind alle drei Per-Klasse-Retry-Caps (stall/unrestrict/generic) gleich dem Limit. Die beiden "15-Fehler"-Shelve-Zweige (download-manager.ts stall-Pfad + error-Pfad) feuern aber auf der hartkodierten Summe >=15 OBERHALB der Per-Klasse-Terminal-Fail-Pruefungen und HALBIEREN danach alle drei Zaehler. Dadurch wurden die Per-Klasse-Caps nie gleichzeitig ueberschritten: der Eintrag erreichte nie status="failed", schliff endlos im 90s-Takt, item.retries wuchs weit ueber das konfigurierte Limit, der Download-Slot blieb dauerhaft belegt, und das wiederholte providerFailures.delete besiegte immer wieder den Provider-Circuit-Breaker → Hoster-Hammering. resetStaleRetryState konnte nicht eingreifen, weil der <=90s-Re-Admit item.updatedAt frisch haelt (nie 10min stale). Der Auslieferungs-Default retryLimit=0 (= unendlich) ist NICHT betroffen — dort ist der 15-Fehler-Shelve der gewollte Dauer-Park-Backstop und es gibt kein endliches Budget zu verletzen. Fix: in BEIDEN Shelve-Zweigen vor dem queueRetry eine harte Obergrenze — `if (configuredRetryLimit > 0 && item.retries >= configuredRetryLimit)` failt den Eintrag terminal (status="failed", recordRunOutcome, retryStateByItem.delete) statt ihn neu zu queuen. Der ∞-Modus (retryLimit<=0) ueberspringt die Grenze und bleibt unveraendert. Runde-6-Audit (adversarisch verifiziert) hat ausserdem den deferred-LOW-Cluster abgeschlossen: #10 HTTP416-shared-counter, #11 fresh-retry-preempt und #13 queue-wait→elapsedMs sind BENIGN; #12 war genau dieser Bug. Rot-bewiesen: Single-Pass-Integrationstest seedet genericErrorRetries=15 vor und injiziert einen generischen Fehler bei retryLimit=5 → ohne Fix requeued der Shelve (status bleibt nicht "failed"), mit Fix wird terminal gefailt. Volle Suite 882 gruen, tsc unveraendert.
This commit is contained in:
@@ -257,6 +257,50 @@ describe("download manager", () => {
|
||||
expect((manager as any).findFallbackProviderNotInCooldown(item)).toBeNull();
|
||||
});
|
||||
|
||||
it("fails an item terminally instead of looping forever when the shelve fires under a finite retryLimit", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-shelve-loop-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "shelve-pkg";
|
||||
const itemId = "shelve-item";
|
||||
const outputDir = path.join(root, "downloads", "Shelve");
|
||||
const extractDir = path.join(root, "extract", "Shelve");
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId, name: "Shelve", outputDir, extractDir,
|
||||
status: "downloading", itemIds: [itemId], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items[itemId] = {
|
||||
id: itemId, packageId, url: "https://hoster.example/flaky.bin", provider: "realdebrid",
|
||||
status: "downloading", retries: 5, speedBps: 0, downloadedBytes: 0, totalBytes: null,
|
||||
progressPercent: 0, fileName: "flaky.bin", targetPath: "", resumable: true,
|
||||
attempts: 0, lastError: "", fullStatus: "", createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", retryLimit: 5, outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session,
|
||||
storagePaths
|
||||
);
|
||||
(manager as any).retryStateByItem.set(itemId, { freshRetryUsed: true, resumeHardResetUsed: true, stallRetries: 0, genericErrorRetries: 15, unrestrictRetries: 0 });
|
||||
(manager as any).debridService.unrestrictLink = async () => { throw new Error("Download-Backend Fehler xyz"); };
|
||||
|
||||
const active = { itemId, packageId, abortController: new AbortController(), abortReason: "none", resumable: true, nonResumableCounted: false, blockedOnDiskWrite: false, blockedOnDiskSince: 0 };
|
||||
(manager as any).activeTasks.set(itemId, active);
|
||||
|
||||
await (manager as any).processItem(active);
|
||||
|
||||
expect(session.items[itemId].status).toBe("failed");
|
||||
expect(session.items[itemId].retries).toBeLessThan(20);
|
||||
});
|
||||
|
||||
it("keeps the quick post-process requeue once the final package items are finished", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-postprocess-final-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user