Add watchdogs for stuck unrestrict and low-throughput downloads

This commit is contained in:
Sucukdeluxe 2026-03-01 04:07:23 +01:00
parent e1e7f63f50
commit 467d4bbc58

View File

@ -49,6 +49,12 @@ const DEFAULT_POST_EXTRACT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
const EXTRACT_PROGRESS_EMIT_INTERVAL_MS = 260; const EXTRACT_PROGRESS_EMIT_INTERVAL_MS = 260;
const DEFAULT_UNRESTRICT_TIMEOUT_MS = 120000;
const DEFAULT_LOW_THROUGHPUT_TIMEOUT_MS = 120000;
const DEFAULT_LOW_THROUGHPUT_MIN_BYTES = 64 * 1024;
function getDownloadStallTimeoutMs(): number { function getDownloadStallTimeoutMs(): number {
const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN); const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) { if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) {
@ -86,6 +92,30 @@ function getPostExtractTimeoutMs(): number {
return DEFAULT_POST_EXTRACT_TIMEOUT_MS; return DEFAULT_POST_EXTRACT_TIMEOUT_MS;
} }
function getUnrestrictTimeoutMs(): number {
const fromEnv = Number(process.env.RD_UNRESTRICT_TIMEOUT_MS ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 5000 && fromEnv <= 15 * 60 * 1000) {
return Math.floor(fromEnv);
}
return DEFAULT_UNRESTRICT_TIMEOUT_MS;
}
function getLowThroughputTimeoutMs(): number {
const fromEnv = Number(process.env.RD_LOW_THROUGHPUT_TIMEOUT_MS ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 30000 && fromEnv <= 20 * 60 * 1000) {
return Math.floor(fromEnv);
}
return DEFAULT_LOW_THROUGHPUT_TIMEOUT_MS;
}
function getLowThroughputMinBytes(): number {
const fromEnv = Number(process.env.RD_LOW_THROUGHPUT_MIN_BYTES ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 1024 && fromEnv <= 32 * 1024 * 1024) {
return Math.floor(fromEnv);
}
return DEFAULT_LOW_THROUGHPUT_MIN_BYTES;
}
type DownloadManagerOptions = { type DownloadManagerOptions = {
megaWebUnrestrict?: MegaWebUnrestrictor; megaWebUnrestrict?: MegaWebUnrestrictor;
}; };
@ -2949,7 +2979,17 @@ export class DownloadManager extends EventEmitter {
const maxUnrestrictRetries = Math.max(3, REQUEST_RETRIES); const maxUnrestrictRetries = Math.max(3, REQUEST_RETRIES);
while (true) { while (true) {
try { try {
const unrestricted = await this.debridService.unrestrictLink(item.url, active.abortController.signal); const unrestrictTimeoutSignal = AbortSignal.timeout(getUnrestrictTimeoutMs());
const unrestrictedSignal = AbortSignal.any([active.abortController.signal, unrestrictTimeoutSignal]);
let unrestricted;
try {
unrestricted = await this.debridService.unrestrictLink(item.url, unrestrictedSignal);
} catch (unrestrictError) {
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
throw new Error(`Unrestrict Timeout nach ${Math.ceil(getUnrestrictTimeoutMs() / 1000)}s`);
}
throw unrestrictError;
}
if (active.abortController.signal.aborted) { if (active.abortController.signal.aborted) {
throw new Error(`aborted:${active.abortReason}`); throw new Error(`aborted:${active.abortReason}`);
} }
@ -3127,10 +3167,15 @@ export class DownloadManager extends EventEmitter {
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = "Paket gestoppt"; item.fullStatus = "Paket gestoppt";
} else if (reason === "stall") { } else if (reason === "stall") {
const stallErrorText = compactErrorText(error);
const isSlowThroughput = stallErrorText.includes("slow_throughput");
active.stallRetries += 1; active.stallRetries += 1;
if (active.stallRetries <= 2) { if (active.stallRetries <= 2) {
item.retries += 1; item.retries += 1;
this.queueRetry(item, active, 350 * active.stallRetries, `Keine Daten empfangen, Retry ${active.stallRetries}/2`); const retryText = isSlowThroughput
? `Zu wenig Datenfluss, Retry ${active.stallRetries}/2`
: `Keine Daten empfangen, Retry ${active.stallRetries}/2`;
this.queueRetry(item, active, 350 * active.stallRetries, retryText);
item.lastError = ""; item.lastError = "";
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
@ -3478,6 +3523,10 @@ export class DownloadManager extends EventEmitter {
const reader = body.getReader(); const reader = body.getReader();
let lastDataAt = nowMs(); let lastDataAt = nowMs();
let lastIdleEmitAt = 0; let lastIdleEmitAt = 0;
const lowThroughputTimeoutMs = getLowThroughputTimeoutMs();
const lowThroughputMinBytes = getLowThroughputMinBytes();
let throughputWindowStartedAt = nowMs();
let throughputWindowBytes = 0;
const idlePulseMs = Math.max(1500, Math.min(3500, Math.floor(stallTimeoutMs / 4) || 2000)); const idlePulseMs = Math.max(1500, Math.min(3500, Math.floor(stallTimeoutMs / 4) || 2000));
const idleTimer = setInterval(() => { const idleTimer = setInterval(() => {
if (active.abortController.signal.aborted) { if (active.abortController.signal.aborted) {
@ -3550,6 +3599,10 @@ export class DownloadManager extends EventEmitter {
this.emitState(); this.emitState();
await sleep(120); await sleep(120);
} }
if (!this.session.paused) {
throughputWindowStartedAt = nowMs();
throughputWindowBytes = 0;
}
if (active.abortController.signal.aborted) { if (active.abortController.signal.aborted) {
throw new Error(`aborted:${active.abortReason}`); throw new Error(`aborted:${active.abortReason}`);
} }
@ -3572,6 +3625,18 @@ export class DownloadManager extends EventEmitter {
this.session.totalDownloadedBytes += buffer.length; this.session.totalDownloadedBytes += buffer.length;
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length); this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
this.recordSpeed(buffer.length); this.recordSpeed(buffer.length);
throughputWindowBytes += buffer.length;
const throughputNow = nowMs();
if (lowThroughputTimeoutMs > 0 && throughputNow - throughputWindowStartedAt >= lowThroughputTimeoutMs) {
if (throughputWindowBytes < lowThroughputMinBytes) {
active.abortReason = "stall";
active.abortController.abort("stall");
throw new Error(`slow_throughput:${throughputWindowBytes}/${lowThroughputMinBytes}`);
}
throughputWindowStartedAt = throughputNow;
throughputWindowBytes = 0;
}
const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.5); const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.5);
const speed = windowBytes / elapsed; const speed = windowBytes / elapsed;