Fix stall-timeout false positives on fully downloaded small archive parts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-07 23:34:46 +01:00
parent 46a64f9e95
commit 00d873445c

View File

@ -4500,6 +4500,15 @@ export class DownloadManager extends EventEmitter {
logger.info(`Post-Process Runde ${round} fertig in ${(roundMs / 1000).toFixed(1)}s (requeue=${hadRequeue}, nextRequeue=${this.hybridExtractRequeue.has(packageId)}): pkg=${packageId.slice(0, 8)}`); logger.info(`Post-Process Runde ${round} fertig in ${(roundMs / 1000).toFixed(1)}s (requeue=${hadRequeue}, nextRequeue=${this.hybridExtractRequeue.has(packageId)}): pkg=${packageId.slice(0, 8)}`);
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
// If this round was very fast (no extraction work, just a
// findReadyArchiveSets scan), consume pending requeues and
// exit the loop. The next download completion will trigger a
// fresh post-processing task. This prevents dozens of no-op
// rounds when many small archive parts complete in rapid
// succession (e.g. 15-20 × 101 MB parts per episode).
if (roundMs < 2000 && this.hybridExtractRequeue.has(packageId)) {
this.hybridExtractRequeue.delete(packageId);
}
} while (this.hybridExtractRequeue.has(packageId)); } while (this.hybridExtractRequeue.has(packageId));
} finally { } finally {
this.releasePostProcessSlot(); this.releasePostProcessSlot();
@ -6702,6 +6711,15 @@ export class DownloadManager extends EventEmitter {
this.recordSpeed(buffer.length, item.packageId); this.recordSpeed(buffer.length, item.packageId);
throughputWindowBytes += buffer.length; throughputWindowBytes += buffer.length;
// All expected bytes received — break immediately instead of waiting
// for the server to close the connection. Some servers/CDNs delay
// the FIN packet, which would trigger the stall timeout even though
// the file is already complete. This especially affects small
// multi-part archives (e.g. 15-20 × 101 MB) on fast connections.
if (item.totalBytes && item.totalBytes > 0 && existingBytes + written >= item.totalBytes) {
break;
}
const throughputNow = nowMs(); const throughputNow = nowMs();
if (lowThroughputTimeoutMs > 0 && throughputNow - throughputWindowStartedAt >= lowThroughputTimeoutMs) { if (lowThroughputTimeoutMs > 0 && throughputNow - throughputWindowStartedAt >= lowThroughputTimeoutMs) {
if (throughputWindowBytes < lowThroughputMinBytes) { if (throughputWindowBytes < lowThroughputMinBytes) {
@ -6742,6 +6760,9 @@ export class DownloadManager extends EventEmitter {
} finally { } finally {
clearInterval(idleTimer); clearInterval(idleTimer);
try { try {
// Cancel pending reads before releasing the lock so the
// underlying TCP connection is torn down promptly.
await reader.cancel().catch(() => {});
reader.releaseLock(); reader.releaseLock();
} catch { } catch {
// ignore // ignore