Add configurable auto-retry limit with optional infinite retries
This commit is contained in:
parent
33e2e126f1
commit
3f17cc8cb4
@ -64,6 +64,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
reconnectWaitSeconds: 45,
|
reconnectWaitSeconds: 45,
|
||||||
completedCleanupPolicy: "never",
|
completedCleanupPolicy: "never",
|
||||||
maxParallel: 4,
|
maxParallel: 4,
|
||||||
|
retryLimit: REQUEST_RETRIES,
|
||||||
speedLimitEnabled: false,
|
speedLimitEnabled: false,
|
||||||
speedLimitKbps: 0,
|
speedLimitKbps: 0,
|
||||||
speedLimitMode: "global",
|
speedLimitMode: "global",
|
||||||
|
|||||||
@ -116,6 +116,22 @@ function getLowThroughputMinBytes(): number {
|
|||||||
return DEFAULT_LOW_THROUGHPUT_MIN_BYTES;
|
return DEFAULT_LOW_THROUGHPUT_MIN_BYTES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRetryLimit(value: unknown): number {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
return REQUEST_RETRIES;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(99, Math.floor(num)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryLimitLabel(retryLimit: number): string {
|
||||||
|
return retryLimit <= 0 ? "inf" : String(retryLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryLimitToMaxRetries(retryLimit: number): number {
|
||||||
|
return retryLimit <= 0 ? Number.MAX_SAFE_INTEGER : retryLimit;
|
||||||
|
}
|
||||||
|
|
||||||
type DownloadManagerOptions = {
|
type DownloadManagerOptions = {
|
||||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
};
|
};
|
||||||
@ -2975,8 +2991,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
active.stallRetries = retryState.stallRetries;
|
active.stallRetries = retryState.stallRetries;
|
||||||
active.genericErrorRetries = retryState.genericErrorRetries;
|
active.genericErrorRetries = retryState.genericErrorRetries;
|
||||||
active.unrestrictRetries = retryState.unrestrictRetries;
|
active.unrestrictRetries = retryState.unrestrictRetries;
|
||||||
const maxGenericErrorRetries = Math.max(2, REQUEST_RETRIES);
|
const configuredRetryLimit = normalizeRetryLimit(this.settings.retryLimit);
|
||||||
const maxUnrestrictRetries = Math.max(3, REQUEST_RETRIES);
|
const retryDisplayLimit = retryLimitLabel(configuredRetryLimit);
|
||||||
|
const maxItemRetries = retryLimitToMaxRetries(configuredRetryLimit);
|
||||||
|
const maxItemAttempts = configuredRetryLimit <= 0 ? Number.MAX_SAFE_INTEGER : maxItemRetries + 1;
|
||||||
|
const maxGenericErrorRetries = maxItemRetries;
|
||||||
|
const maxUnrestrictRetries = maxItemRetries;
|
||||||
|
const maxStallRetries = maxItemRetries;
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const unrestrictTimeoutSignal = AbortSignal.timeout(getUnrestrictTimeoutMs());
|
const unrestrictTimeoutSignal = AbortSignal.timeout(getUnrestrictTimeoutMs());
|
||||||
@ -3015,7 +3036,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
|
|
||||||
const maxAttempts = REQUEST_RETRIES;
|
const maxAttempts = maxItemAttempts;
|
||||||
let done = false;
|
let done = false;
|
||||||
while (!done && item.attempts < maxAttempts) {
|
while (!done && item.attempts < maxAttempts) {
|
||||||
item.attempts += 1;
|
item.attempts += 1;
|
||||||
@ -3170,11 +3191,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const stallErrorText = compactErrorText(error);
|
const stallErrorText = compactErrorText(error);
|
||||||
const isSlowThroughput = stallErrorText.includes("slow_throughput");
|
const isSlowThroughput = stallErrorText.includes("slow_throughput");
|
||||||
active.stallRetries += 1;
|
active.stallRetries += 1;
|
||||||
if (active.stallRetries <= 2) {
|
if (active.stallRetries <= maxStallRetries) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
const retryText = isSlowThroughput
|
const retryText = isSlowThroughput
|
||||||
? `Zu wenig Datenfluss, Retry ${active.stallRetries}/2`
|
? `Zu wenig Datenfluss, Retry ${active.stallRetries}/${retryDisplayLimit}`
|
||||||
: `Keine Daten empfangen, Retry ${active.stallRetries}/2`;
|
: `Keine Daten empfangen, Retry ${active.stallRetries}/${retryDisplayLimit}`;
|
||||||
this.queueRetry(item, active, 350 * active.stallRetries, retryText);
|
this.queueRetry(item, active, 350 * active.stallRetries, retryText);
|
||||||
item.lastError = "";
|
item.lastError = "";
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
@ -3277,9 +3298,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
throw new Error("Download-Item fehlt");
|
throw new Error("Download-Item fehlt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configuredRetryLimit = normalizeRetryLimit(this.settings.retryLimit);
|
||||||
|
const retryDisplayLimit = retryLimitLabel(configuredRetryLimit);
|
||||||
|
const maxAttempts = configuredRetryLimit <= 0 ? Number.MAX_SAFE_INTEGER : configuredRetryLimit + 1;
|
||||||
|
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
let effectiveTargetPath = targetPath;
|
let effectiveTargetPath = targetPath;
|
||||||
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
let existingBytes = 0;
|
let existingBytes = 0;
|
||||||
try {
|
try {
|
||||||
const stat = await fs.promises.stat(effectiveTargetPath);
|
const stat = await fs.promises.stat(effectiveTargetPath);
|
||||||
@ -3322,9 +3347,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
lastError = compactErrorText(error);
|
lastError = compactErrorText(error);
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < maxAttempts) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
item.fullStatus = `Verbindungsfehler, retry ${attempt + 1}/${REQUEST_RETRIES}`;
|
item.fullStatus = `Verbindungsfehler, retry ${attempt}/${retryDisplayLimit}`;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
await sleep(300 * attempt);
|
await sleep(300 * attempt);
|
||||||
continue;
|
continue;
|
||||||
@ -3360,10 +3385,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.totalBytes = knownTotal && knownTotal > 0 ? knownTotal : null;
|
item.totalBytes = knownTotal && knownTotal > 0 ? knownTotal : null;
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.fullStatus = `Range-Konflikt (HTTP 416), starte neu ${Math.min(REQUEST_RETRIES, attempt + 1)}/${REQUEST_RETRIES}`;
|
item.fullStatus = `Range-Konflikt (HTTP 416), starte neu ${attempt}/${retryDisplayLimit}`;
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < maxAttempts) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
await sleep(280 * attempt);
|
await sleep(280 * attempt);
|
||||||
continue;
|
continue;
|
||||||
@ -3380,9 +3405,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (this.settings.autoReconnect && [429, 503].includes(response.status)) {
|
if (this.settings.autoReconnect && [429, 503].includes(response.status)) {
|
||||||
this.requestReconnect(`HTTP ${response.status}`);
|
this.requestReconnect(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < maxAttempts) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
item.fullStatus = `Serverfehler ${response.status}, retry ${attempt + 1}/${REQUEST_RETRIES}`;
|
item.fullStatus = `Serverfehler ${response.status}, retry ${attempt}/${retryDisplayLimit}`;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
await sleep(350 * attempt);
|
await sleep(350 * attempt);
|
||||||
continue;
|
continue;
|
||||||
@ -3593,13 +3618,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (active.abortController.signal.aborted) {
|
if (active.abortController.signal.aborted) {
|
||||||
throw new Error(`aborted:${active.abortReason}`);
|
throw new Error(`aborted:${active.abortReason}`);
|
||||||
}
|
}
|
||||||
|
let pausedDuringWait = false;
|
||||||
while (this.session.paused && this.session.running && !active.abortController.signal.aborted) {
|
while (this.session.paused && this.session.running && !active.abortController.signal.aborted) {
|
||||||
|
pausedDuringWait = true;
|
||||||
item.status = "paused";
|
item.status = "paused";
|
||||||
item.fullStatus = "Pausiert";
|
item.fullStatus = "Pausiert";
|
||||||
this.emitState();
|
this.emitState();
|
||||||
await sleep(120);
|
await sleep(120);
|
||||||
}
|
}
|
||||||
if (!this.session.paused) {
|
if (pausedDuringWait) {
|
||||||
throughputWindowStartedAt = nowMs();
|
throughputWindowStartedAt = nowMs();
|
||||||
throughputWindowBytes = 0;
|
throughputWindowBytes = 0;
|
||||||
}
|
}
|
||||||
@ -3709,9 +3736,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
lastError = compactErrorText(error);
|
lastError = compactErrorText(error);
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < maxAttempts) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
item.fullStatus = `Downloadfehler, retry ${attempt + 1}/${REQUEST_RETRIES}`;
|
item.fullStatus = `Downloadfehler, retry ${attempt}/${retryDisplayLimit}`;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
await sleep(350 * attempt);
|
await sleep(350 * attempt);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -96,6 +96,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
autoResumeOnStart: Boolean(settings.autoResumeOnStart),
|
autoResumeOnStart: Boolean(settings.autoResumeOnStart),
|
||||||
autoReconnect: Boolean(settings.autoReconnect),
|
autoReconnect: Boolean(settings.autoReconnect),
|
||||||
maxParallel: clampNumber(settings.maxParallel, defaults.maxParallel, 1, 50),
|
maxParallel: clampNumber(settings.maxParallel, defaults.maxParallel, 1, 50),
|
||||||
|
retryLimit: clampNumber(settings.retryLimit, defaults.retryLimit, 0, 99),
|
||||||
reconnectWaitSeconds: clampNumber(settings.reconnectWaitSeconds, defaults.reconnectWaitSeconds, 10, 600),
|
reconnectWaitSeconds: clampNumber(settings.reconnectWaitSeconds, defaults.reconnectWaitSeconds, 10, 600),
|
||||||
completedCleanupPolicy: settings.completedCleanupPolicy,
|
completedCleanupPolicy: settings.completedCleanupPolicy,
|
||||||
speedLimitEnabled: Boolean(settings.speedLimitEnabled),
|
speedLimitEnabled: Boolean(settings.speedLimitEnabled),
|
||||||
|
|||||||
@ -53,7 +53,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
|||||||
cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false,
|
cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false,
|
||||||
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
|
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
|
||||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||||
maxParallel: 4, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
maxParallel: 4, retryLimit: 3, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||||
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
|
||||||
theme: "dark", bandwidthSchedules: []
|
theme: "dark", bandwidthSchedules: []
|
||||||
},
|
},
|
||||||
@ -1497,6 +1497,7 @@ export function App(): ReactElement {
|
|||||||
<h3>Queue, Limits & Reconnect</h3>
|
<h3>Queue, Limits & Reconnect</h3>
|
||||||
<div className="field-grid two">
|
<div className="field-grid two">
|
||||||
<div><label>Max. Downloads</label><input type="number" min={1} max={50} value={settingsDraft.maxParallel} onChange={(e) => setNum("maxParallel", Number(e.target.value) || 1)} /></div>
|
<div><label>Max. Downloads</label><input type="number" min={1} max={50} value={settingsDraft.maxParallel} onChange={(e) => setNum("maxParallel", Number(e.target.value) || 1)} /></div>
|
||||||
|
<div><label>Auto-Retry Limit (0 = inf)</label><input type="number" min={0} max={99} value={settingsDraft.retryLimit} onChange={(e) => setNum("retryLimit", Math.max(0, Math.min(99, Number(e.target.value) || 0)))} /></div>
|
||||||
<div><label>Reconnect-Wartezeit (Sek.)</label><input type="number" min={10} max={600} value={settingsDraft.reconnectWaitSeconds} onChange={(e) => setNum("reconnectWaitSeconds", Number(e.target.value) || 45)} /></div>
|
<div><label>Reconnect-Wartezeit (Sek.)</label><input type="number" min={10} max={600} value={settingsDraft.reconnectWaitSeconds} onChange={(e) => setNum("reconnectWaitSeconds", Number(e.target.value) || 45)} /></div>
|
||||||
</div>
|
</div>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.speedLimitEnabled} onChange={(e) => setBool("speedLimitEnabled", e.target.checked)} /> Speed-Limit aktivieren</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.speedLimitEnabled} onChange={(e) => setBool("speedLimitEnabled", e.target.checked)} /> Speed-Limit aktivieren</label>
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export interface AppSettings {
|
|||||||
reconnectWaitSeconds: number;
|
reconnectWaitSeconds: number;
|
||||||
completedCleanupPolicy: FinishedCleanupPolicy;
|
completedCleanupPolicy: FinishedCleanupPolicy;
|
||||||
maxParallel: number;
|
maxParallel: number;
|
||||||
|
retryLimit: number;
|
||||||
speedLimitEnabled: boolean;
|
speedLimitEnabled: boolean;
|
||||||
speedLimitKbps: number;
|
speedLimitKbps: number;
|
||||||
speedLimitMode: SpeedMode;
|
speedLimitMode: SpeedMode;
|
||||||
|
|||||||
@ -80,6 +80,7 @@ describe("settings storage", () => {
|
|||||||
completedCleanupPolicy: "broken" as unknown as AppSettings["completedCleanupPolicy"],
|
completedCleanupPolicy: "broken" as unknown as AppSettings["completedCleanupPolicy"],
|
||||||
speedLimitMode: "broken" as unknown as AppSettings["speedLimitMode"],
|
speedLimitMode: "broken" as unknown as AppSettings["speedLimitMode"],
|
||||||
maxParallel: 0,
|
maxParallel: 0,
|
||||||
|
retryLimit: 999,
|
||||||
reconnectWaitSeconds: 9999,
|
reconnectWaitSeconds: 9999,
|
||||||
speedLimitKbps: -1,
|
speedLimitKbps: -1,
|
||||||
outputDir: " ",
|
outputDir: " ",
|
||||||
@ -96,6 +97,7 @@ describe("settings storage", () => {
|
|||||||
expect(normalized.completedCleanupPolicy).toBe("never");
|
expect(normalized.completedCleanupPolicy).toBe("never");
|
||||||
expect(normalized.speedLimitMode).toBe("global");
|
expect(normalized.speedLimitMode).toBe("global");
|
||||||
expect(normalized.maxParallel).toBe(1);
|
expect(normalized.maxParallel).toBe(1);
|
||||||
|
expect(normalized.retryLimit).toBe(99);
|
||||||
expect(normalized.reconnectWaitSeconds).toBe(600);
|
expect(normalized.reconnectWaitSeconds).toBe(600);
|
||||||
expect(normalized.speedLimitKbps).toBe(0);
|
expect(normalized.speedLimitKbps).toBe(0);
|
||||||
expect(normalized.outputDir).toBe(defaultSettings().outputDir);
|
expect(normalized.outputDir).toBe(defaultSettings().outputDir);
|
||||||
@ -115,6 +117,7 @@ describe("settings storage", () => {
|
|||||||
providerPrimary: "not-valid",
|
providerPrimary: "not-valid",
|
||||||
completedCleanupPolicy: "not-valid",
|
completedCleanupPolicy: "not-valid",
|
||||||
maxParallel: "999",
|
maxParallel: "999",
|
||||||
|
retryLimit: "-3",
|
||||||
reconnectWaitSeconds: "1",
|
reconnectWaitSeconds: "1",
|
||||||
speedLimitMode: "not-valid",
|
speedLimitMode: "not-valid",
|
||||||
updateRepo: ""
|
updateRepo: ""
|
||||||
@ -126,6 +129,7 @@ describe("settings storage", () => {
|
|||||||
expect(loaded.providerPrimary).toBe("realdebrid");
|
expect(loaded.providerPrimary).toBe("realdebrid");
|
||||||
expect(loaded.completedCleanupPolicy).toBe("never");
|
expect(loaded.completedCleanupPolicy).toBe("never");
|
||||||
expect(loaded.maxParallel).toBe(50);
|
expect(loaded.maxParallel).toBe(50);
|
||||||
|
expect(loaded.retryLimit).toBe(0);
|
||||||
expect(loaded.reconnectWaitSeconds).toBe(10);
|
expect(loaded.reconnectWaitSeconds).toBe(10);
|
||||||
expect(loaded.speedLimitMode).toBe("global");
|
expect(loaded.speedLimitMode).toBe("global");
|
||||||
expect(loaded.updateRepo).toBe(defaultSettings().updateRepo);
|
expect(loaded.updateRepo).toBe(defaultSettings().updateRepo);
|
||||||
@ -312,6 +316,7 @@ describe("settings storage", () => {
|
|||||||
const defaults = defaultSettings();
|
const defaults = defaultSettings();
|
||||||
expect(loaded.providerPrimary).toBe(defaults.providerPrimary);
|
expect(loaded.providerPrimary).toBe(defaults.providerPrimary);
|
||||||
expect(loaded.maxParallel).toBe(defaults.maxParallel);
|
expect(loaded.maxParallel).toBe(defaults.maxParallel);
|
||||||
|
expect(loaded.retryLimit).toBe(defaults.retryLimit);
|
||||||
expect(loaded.outputDir).toBe(defaults.outputDir);
|
expect(loaded.outputDir).toBe(defaults.outputDir);
|
||||||
expect(loaded.cleanupMode).toBe(defaults.cleanupMode);
|
expect(loaded.cleanupMode).toBe(defaults.cleanupMode);
|
||||||
});
|
});
|
||||||
@ -420,6 +425,7 @@ describe("settings storage", () => {
|
|||||||
expect(loaded.speedLimitMode).toBe(defaults.speedLimitMode);
|
expect(loaded.speedLimitMode).toBe(defaults.speedLimitMode);
|
||||||
expect(loaded.clipboardWatch).toBe(defaults.clipboardWatch);
|
expect(loaded.clipboardWatch).toBe(defaults.clipboardWatch);
|
||||||
expect(loaded.minimizeToTray).toBe(defaults.minimizeToTray);
|
expect(loaded.minimizeToTray).toBe(defaults.minimizeToTray);
|
||||||
|
expect(loaded.retryLimit).toBe(defaults.retryLimit);
|
||||||
expect(loaded.collectMkvToLibrary).toBe(defaults.collectMkvToLibrary);
|
expect(loaded.collectMkvToLibrary).toBe(defaults.collectMkvToLibrary);
|
||||||
expect(loaded.mkvLibraryDir).toBe(defaults.mkvLibraryDir);
|
expect(loaded.mkvLibraryDir).toBe(defaults.mkvLibraryDir);
|
||||||
expect(loaded.theme).toBe(defaults.theme);
|
expect(loaded.theme).toBe(defaults.theme);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user