Fix download freeze spikes and unrestrict slot overshoot handling

This commit is contained in:
Sucukdeluxe
2026-03-02 23:47:54 +01:00
parent 7fe7d93e83
commit 19342647e5
10 changed files with 230 additions and 38 deletions
+45 -6
View File
@@ -242,6 +242,20 @@ function isUnrestrictFailure(errorText: string): boolean {
|| text.includes("session") || text.includes("login");
}
function isProviderBusyUnrestrictError(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return text.includes("too many active")
|| text.includes("too many concurrent")
|| text.includes("too many downloads")
|| text.includes("active download")
|| text.includes("concurrent limit")
|| text.includes("slot limit")
|| text.includes("limit reached")
|| text.includes("zu viele aktive")
|| text.includes("zu viele gleichzeitige")
|| text.includes("zu viele downloads");
}
function isFinishedStatus(status: DownloadStatus): boolean {
return status === "completed" || status === "failed" || status === "cancelled";
}
@@ -3126,6 +3140,15 @@ export class DownloadManager extends EventEmitter {
}
}
private applyProviderBusyBackoff(provider: string, cooldownMs: number): void {
const key = String(provider || "").trim() || "unknown";
const now = nowMs();
const entry = this.providerFailures.get(key) || { count: 0, lastFailAt: 0, cooldownUntil: 0 };
entry.lastFailAt = now;
entry.cooldownUntil = Math.max(entry.cooldownUntil, now + Math.max(0, Math.floor(cooldownMs)));
this.providerFailures.set(key, entry);
}
private getProviderCooldownRemaining(provider: string): number {
const entry = this.providerFailures.get(provider);
if (!entry || entry.cooldownUntil <= 0) {
@@ -3498,9 +3521,17 @@ export class DownloadManager extends EventEmitter {
if (!item || !pkg || pkg.cancelled || !pkg.enabled) {
return;
}
if (item.status !== "queued" && item.status !== "reconnect_wait") {
return;
}
if (this.activeTasks.has(itemId)) {
return;
}
const maxParallel = Math.max(1, Number(this.settings.maxParallel) || 1);
if (this.activeTasks.size >= maxParallel) {
logger.warn(`startItem übersprungen (Parallel-Limit): active=${this.activeTasks.size}, max=${maxParallel}, item=${item.fileName || item.id}`);
return;
}
this.retryAfterByItem.delete(itemId);
@@ -3580,8 +3611,7 @@ export class DownloadManager extends EventEmitter {
throw new Error(`aborted:${active.abortReason}`);
}
// Check provider cooldown before attempting unrestrict
const lastProvider = item.provider || "";
const cooldownProvider = lastProvider || this.settings.providerPrimary || "unknown";
const cooldownProvider = item.provider || this.settings.providerPrimary || "unknown";
const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider);
if (cooldownMs > 0) {
const delayMs = Math.min(cooldownMs + 1000, 310000);
@@ -3598,13 +3628,17 @@ export class DownloadManager extends EventEmitter {
} catch (unrestrictError) {
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
// Record failure for all providers since we don't know which one timed out
this.recordProviderFailure(lastProvider || "unknown");
this.recordProviderFailure(cooldownProvider);
throw new Error(`Unrestrict Timeout nach ${Math.ceil(getUnrestrictTimeoutMs() / 1000)}s`);
}
// Record failure for the provider that errored
const errText = compactErrorText(unrestrictError);
if (isUnrestrictFailure(errText)) {
this.recordProviderFailure(lastProvider || "unknown");
this.recordProviderFailure(cooldownProvider);
if (isProviderBusyUnrestrictError(errText)) {
const busyCooldownMs = Math.min(60000, 12000 + Number(active.unrestrictRetries || 0) * 3000);
this.applyProviderBusyBackoff(cooldownProvider, busyCooldownMs);
}
}
throw unrestrictError;
}
@@ -3951,11 +3985,16 @@ export class DownloadManager extends EventEmitter {
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1;
item.retries += 1;
this.recordProviderFailure(item.provider || "unknown");
const failureProvider = item.provider || this.settings.providerPrimary || "unknown";
this.recordProviderFailure(failureProvider);
if (isProviderBusyUnrestrictError(errorText)) {
const busyCooldownMs = Math.min(60000, 12000 + Number(active.unrestrictRetries || 0) * 3000);
this.applyProviderBusyBackoff(failureProvider, busyCooldownMs);
}
// Escalating backoff: 5s, 7.5s, 11s, 17s, 25s, 38s, ... up to 120s
let unrestrictDelayMs = Math.min(120000, Math.floor(5000 * Math.pow(1.5, active.unrestrictRetries - 1)));
// Respect provider cooldown
const providerCooldown = this.getProviderCooldownRemaining(item.provider || "unknown");
const providerCooldown = this.getProviderCooldownRemaining(failureProvider);
if (providerCooldown > unrestrictDelayMs) {
unrestrictDelayMs = providerCooldown + 1000;
}