Härtung Download/Rotation (Audit-Batch 1): Scheduler-Freeze, Cooldown-Respekt, Daily-Reset, Kategorisierung

Aus einem adversarisch verifizierten Multi-Agent-Audit (14 confirmed/11 refuted):

#1 HIGH Scheduler-Freeze: findNextQueuedItem hatte keinen activeTasks-Guard.
Wird ein Item zurueckgesetzt/ueberschrieben, waehrend sein alter Task noch in
einem nicht-abbrechbaren await parkt (z.B. Integritaets-Check), liefert
findNextQueuedItem dasselbe Item, startItem lehnt es ab ohne activeTasks zu
verkleinern → der SYNCHRONE Admission-Loop dreht endlos → Event-Loop friert
permanent ein. Fix: `if (this.activeTasks.has(itemId)) continue;`. Repro-Test
(ohne Fix haengt sogar der vitest-Timeout — Freeze bewiesen).

#2/#3 MED mega_debrid_cooldown:<ms> wurde verworfen: kein Parser (nur das
debrid_link-Analogon) → Item lief in den generischen 5s-Exponential-Backoff
und fragte cooled Accounts im Sekundentakt erneut an. Neuer
parseMegaDebridCooldownRetry (nimmt das frueheste Cooldown-Ende ueber alle
Accounts) + Handler VOR transient/generic → Item wartet die echte Cooldown-Zeit.

#7/#8 MED Mega per-Account Tages-Usage wurde am Tageswechsel nie zurueckgesetzt
(ensureProviderDailyUsageFresh ruecksetzte nur provider/debrid-link), und weil
es den Tagesschluessel zuerst weiterstellte, lief auch der Reset in
addMegaDebridAccountDailyUsageBytes ins Leere → Accounts blieben den ganzen Tag
faelschlich "am Limit" und schrumpften das (neue) Pro-Account-Umwandlungslimit.
Fix: megaDebridAccountDailyUsageBytes im Tageswechsel mit zuruecksetzen.

#14 LOW classifyAccountFailure: rate_limit-Branch vor quota (quota matchte
"limit" in "rate limit" → Fehl-Kategorisierung).

852/852 gruen, tsc unveraendert (6). #4 (empty→until-restart) bewusst deferred.
This commit is contained in:
Sucukdeluxe
2026-06-17 04:22:20 +02:00
parent a7d989ebd2
commit 3bd6e3b23e
6 changed files with 262 additions and 10 deletions
+9 -9
View File
@@ -2183,6 +2183,15 @@ class MegaDebridClient {
return { fatal: true, cooldownMs: 0, message: errorText, category: "skip" };
}
if (/rate.?limit|too.?many|429/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: `Rate-Limit (${errorText})`,
category: "rate_limit"
};
}
if (/quota|limit|exceeded|bandwidth/i.test(errorText)) {
return {
fatal: false,
@@ -2202,15 +2211,6 @@ class MegaDebridClient {
};
}
if (/rate.?limit|too.?many|429/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: `Rate-Limit (${errorText})`,
category: "rate_limit"
};
}
if (/antwort\s+leer|empty\s+response|leere\s+antwort/i.test(errorText)) {
return {
fatal: false,
+33
View File
@@ -645,6 +645,20 @@ function parseDebridLinkCooldownRetry(errorText: string): { delayMs: number; det
return { delayMs, detail };
}
export function parseMegaDebridCooldownRetry(errorText: string): { delayMs: number; detail: string } | null {
const text = String(errorText || "");
const matches = [...text.matchAll(/mega_debrid_cooldown:(\d+)/gi)];
if (matches.length === 0) {
return null;
}
const delays = matches.map((m) => Number(m[1])).filter((n) => Number.isFinite(n) && n > 0);
if (delays.length === 0) {
return null;
}
const delayMs = Math.max(1000, Math.min(15 * 60 * 1000, Math.min(...delays)));
return { delayMs, detail: text.replace(/mega_debrid_cooldown:\d+:/i, "").trim() };
}
function parseDebridLinkTerminalFailure(errorText: string): { kind: "invalid_all" | "no_active_key"; detail: string } | null {
const raw = String(errorText || "");
const match = raw.match(/debrid_link_(invalid_all|no_active_key):(.*)$/i);
@@ -7764,6 +7778,7 @@ export class DownloadManager extends EventEmitter {
this.settings.providerDailyUsageDay = currentDay;
this.settings.providerDailyUsageBytes = {};
this.settings.debridLinkApiKeyDailyUsageBytes = {};
this.settings.megaDebridAccountDailyUsageBytes = {};
this.statsCache = null;
this.statsCacheAt = 0;
if (persist) {
@@ -8474,6 +8489,7 @@ export class DownloadManager extends EventEmitter {
const retryAfter = this.retryAfterByItem.get(itemId) || 0;
if (retryAfter > now) continue;
if (item.status !== "queued" && item.status !== "reconnect_wait") continue;
if (this.activeTasks.has(itemId)) continue;
if (this.delayPacedStartForItem(item, now)) continue;
if (this.shouldDelayStartForItem(item)) continue;
@@ -9349,6 +9365,23 @@ export class DownloadManager extends EventEmitter {
return;
}
const megaCooldownRetry = parseMegaDebridCooldownRetry(errorText);
if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1;
item.retries += 1;
logger.warn(`Mega-Debrid Account-Cooldown: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaCooldownRetry.delayMs}ms, link=${item.url.slice(0, 80)}`);
this.queueRetry(
item,
active,
megaCooldownRetry.delayMs,
`Mega-Debrid Cooldown, neuer Versuch in ${Math.ceil(megaCooldownRetry.delayMs / 1000)}s`
);
item.lastError = megaCooldownRetry.detail || errorText;
this.persistSoon();
this.emitState();
return;
}
if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1;
item.retries += 1;