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:
parent
e49ed1ada0
commit
d2a1b831a0
@ -9165,6 +9165,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const totalFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
|
const totalFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
|
||||||
if (totalFailures >= 15) {
|
if (totalFailures >= 15) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
|
if (configuredRetryLimit > 0 && item.retries >= configuredRetryLimit) {
|
||||||
|
item.status = "failed";
|
||||||
|
item.lastError = stallErrorText || "Wiederholt fehlgeschlagen";
|
||||||
|
item.fullStatus = `Fehler: ${item.lastError}`;
|
||||||
|
item.speedBps = 0;
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
this.recordRunOutcome(item.id, "failed");
|
||||||
|
this.retryStateByItem.delete(item.id);
|
||||||
|
const shelveFailPkgStall = this.session.packages[item.packageId];
|
||||||
|
if (shelveFailPkgStall) this.refreshPackageStatus(shelveFailPkgStall);
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
|
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
|
||||||
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
|
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
|
||||||
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
|
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
|
||||||
@ -9351,6 +9365,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const totalNonStallFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
|
const totalNonStallFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
|
||||||
if (totalNonStallFailures >= 15) {
|
if (totalNonStallFailures >= 15) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
|
if (configuredRetryLimit > 0 && item.retries >= configuredRetryLimit) {
|
||||||
|
item.status = "failed";
|
||||||
|
item.lastError = errorText || "Wiederholt fehlgeschlagen";
|
||||||
|
item.fullStatus = `Fehler: ${item.lastError}`;
|
||||||
|
item.speedBps = 0;
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
this.recordRunOutcome(item.id, "failed");
|
||||||
|
this.retryStateByItem.delete(item.id);
|
||||||
|
const shelveFailPkgErr = this.session.packages[item.packageId];
|
||||||
|
if (shelveFailPkgErr) this.refreshPackageStatus(shelveFailPkgErr);
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
|
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
|
||||||
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
|
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
|
||||||
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
|
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
|
||||||
|
|||||||
@ -161,6 +161,26 @@ RELEASE BEWUSST AUFGESCHOBEN: beide MED/LOW (kein HIGH wie 216/217) → buendeln
|
|||||||
Retries ueberlastet → kein Sign-off fuer den Mega-Park-Hot-Path-Klassifikations-Change. Fixes sind
|
Retries ueberlastet → kein Sign-off fuer den Mega-Park-Hot-Path-Klassifikations-Change. Fixes sind
|
||||||
durable. Plan: Runde 6 dazu buendeln, Advisor-Sign-off abwarten, dann v1.7.218 als Roll-up.
|
durable. Plan: Runde 6 dazu buendeln, Advisor-Sign-off abwarten, dann v1.7.218 als Roll-up.
|
||||||
|
|
||||||
|
## Runde 6 (Retry/Backoff/Error-Klassifikations-State-Machine + Disk/IO) — Workflow wqurq83ma
|
||||||
|
3 Finder + adversarisch verifizieren. 1 confirmed (2/3), 3 refuted. Schliesst den deferred-LOW-Cluster ab.
|
||||||
|
- **CONFIRMED HIGH (GEFIXT) SHELVE-LOOP-RETRYLIMIT:** Bei FINITEM retryLimit>=5 sind alle drei Per-Klasse-
|
||||||
|
Caps = retryLimit; die 15-Failure-Shelve-Zweige (dl-mgr 9166 stall + 9352 error) feuern aber auf
|
||||||
|
hartkodiertem `sum>=15` OBERHALB der Per-Klasse-Terminal-Fails und HALBIEREN danach alle Counter →
|
||||||
|
Per-Klasse-Caps werden nie gleichzeitig ueberschritten → Item failt NIE, schleift ewig, item.retries
|
||||||
|
waechst ueber das konfigurierte Limit, Slot gestrandet, providerFailures.delete besiegt wiederholt den
|
||||||
|
Circuit-Breaker → Hoster-Hammering. resetStaleRetryState rettet nicht (<=90s-Re-Admit haelt updatedAt
|
||||||
|
frisch < 10min-Stale). Default retryLimit=0=∞ NICHT betroffen (Shelve ist dort der gewollte Park-Backstop).
|
||||||
|
Fix: in BEIDEN Shelve-Zweigen vor dem queueRetry `if (configuredRetryLimit > 0 && item.retries >=
|
||||||
|
configuredRetryLimit)` → terminal failen statt requeue (∞-Modus unberuehrt). Rot-bewiesen (Single-Pass-
|
||||||
|
Integrationstest: genericErrorRetries=15 vorgeseedet + injizierter Generic-Error → status "failed" statt
|
||||||
|
requeue). tsc=6.
|
||||||
|
- **Deferred-LOW-Cluster re-klassifiziert (aus Runden 1-2):** #10 HTTP416-shared-counter BENIGN (maxHttp416Retries
|
||||||
|
eigenes Budget, speist Shelve-Summe NICHT); #11 fresh-retry-preempt BENIGN (One-Shot-Booleans);
|
||||||
|
STALL-CAP-OFF-BY-ONE real aber +1 (Zutat von CONFIRMED-1, kein eigener Strand); #13 queue-wait→elapsedMs
|
||||||
|
BENIGN (queueRetry resettet attempts/updatedAt, Stall-Detektor misst nur aktiven Download). #12 = der
|
||||||
|
gefixte Bug. DISK-1 (ENOSPC/EACCES via Generic-Budget) kein eigener Bug (in finite vom Cap + diesem Fix
|
||||||
|
begrenzt; in ∞ by-design) → optionale Klassifikations-Erweiterung, deferred bis Nutzer Full-Disk-Hammering meldet.
|
||||||
|
|
||||||
## Runde 5 (unberuehrte Subsysteme: auto-reconnect, Scheduler-Fairness, Crash/Persistenz) — Workflow w7woztsxx
|
## Runde 5 (unberuehrte Subsysteme: auto-reconnect, Scheduler-Fairness, Crash/Persistenz) — Workflow w7woztsxx
|
||||||
3 Finder (reconnect / scheduler / persistence) → adversarisch verifizieren (3 Lenses). 1 Kandidat, 0 confirmed.
|
3 Finder (reconnect / scheduler / persistence) → adversarisch verifizieren (3 Lenses). 1 Kandidat, 0 confirmed.
|
||||||
**Sauberes „kein bestaetigter Bug"-Ergebnis.** Auto-reconnect-Resume, Slot-Accounting/Fairness und
|
**Sauberes „kein bestaetigter Bug"-Ergebnis.** Auto-reconnect-Resume, Slot-Accounting/Fairness und
|
||||||
|
|||||||
@ -63,6 +63,17 @@ ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration n
|
|||||||
|
|
||||||
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
|
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
|
||||||
|
|
||||||
|
- **Endlos-Wiederholung bei festem Wiederholungslimit behoben:** Wenn du ein FESTES Retry-Limit
|
||||||
|
(z.B. 5) eingestellt hattest UND ein Link sprunghaft verschiedene Fehlerarten produzierte
|
||||||
|
(mal Umwandlungs-Timeout, mal Abbruch mitten im Download, mal allgemeiner Fehler), konnte ein
|
||||||
|
Eintrag in einer Endlosschleife hängen: eine interne „Viele-Fehler"-Pause halbierte die Zähler,
|
||||||
|
sodass das eingestellte Limit nie erreicht wurde — der Eintrag scheiterte nie, blockierte dauerhaft
|
||||||
|
einen Download-Slot und hämmerte den Anbieter (weil dabei auch die Anbieter-Sperre zurückgesetzt
|
||||||
|
wurde). Jetzt wird das von dir eingestellte Limit hart eingehalten: nach N Versuchen scheitert der
|
||||||
|
Eintrag sauber. Standard-Einstellung („unendlich", der Auslieferungs-Default) war nie betroffen.
|
||||||
|
Rot-bewiesener Test.
|
||||||
|
|
||||||
|
|
||||||
- **Mega „bis Tagesreset gesperrt" parkt jetzt wirklich (statt alle 2 min neu zu versuchen):**
|
- **Mega „bis Tagesreset gesperrt" parkt jetzt wirklich (statt alle 2 min neu zu versuchen):**
|
||||||
Wenn ALLE Mega-Accounts wegen wiederholt leerer Antworten bis zum Tagesreset geparkt waren,
|
Wenn ALLE Mega-Accounts wegen wiederholt leerer Antworten bis zum Tagesreset geparkt waren,
|
||||||
hat das Tool den Fehler bisher als normalen Umwandlungsfehler behandelt und den ganzen Tag
|
hat das Tool den Fehler bisher als normalen Umwandlungsfehler behandelt und den ganzen Tag
|
||||||
|
|||||||
@ -257,6 +257,50 @@ describe("download manager", () => {
|
|||||||
expect((manager as any).findFallbackProviderNotInCooldown(item)).toBeNull();
|
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", () => {
|
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-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-postprocess-final-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user