From d77dfbedecda3c5a4e8becd351a4031859c1d542 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Thu, 11 Jun 2026 14:33:20 +0200 Subject: [PATCH] Fix: Langsame Mega-Accounts bremsen nicht mehr die ganze Umwandlungs-Queue (Latenz-Demotion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folge-Fix zu v1.7.197 (Round-Robin): User meldete direkt nach dem Update deutlich langsamere Link-Umwandlung. Ursache: MegaWebFallback.runExclusive ist eine GLOBALE Single-Flight-Queue — alle Umwandlungen laufen seriell. Vor v1.7.197 liefen praktisch alle Links ueber denselben warmen, schnellen Account (~800ms); das Round-Robin mischte nun auch die zuvor nie genutzten Accounts in die Reihe. Ist einer davon langsam (kalte Session, traegere Server, abgelaufenes Premium), blockiert seine Umwandlung in der seriellen Queue ALLE nachfolgenden Links — gefuehlt wird alles langsam. Fix: Pro Account+Modus wird ein EMA (0.7/0.3) der erfolgreichen Unrestrict-Dauer gefuehrt. Bei jeder Aufloesung wird die Round-Robin- Reihenfolge partitioniert: Accounts, deren EMA ueber max(6s, 3x bestes verfuegbares EMA) liegt, wandern ans Ende der Reihe — sie bleiben Failover-Reserve (werden weiter genutzt, wenn die schnellen am Limit/Cooldown sind), laufen aber nicht mehr in der gleichmaessigen Verteilung mit. Ungemessene Accounts gelten als gesund (bekommen ihre Chance und damit ein EMA). Erholt sich ein Account (relativer Threshold), rotiert er automatisch wieder mit. Schon nach EINEM langsamen Erfolg ist ein Bremser-Account aus der Verteilung draussen. Fehlschlaege werden wie bisher ueber die bestehenden Cooldowns behandelt. Diagnose-Sichtbarkeit: das TEST-Event im account-rotation.log traegt jetzt emaMs und slow=true, damit ein gebremster Account sofort erkennbar ist. 2 neue Regressionstests (langsamer Account wird depriorisiert; bleibt Failover-Reserve wenn die schnellen gesperrt sind). Suite 808 gruen, tsc=6 Baseline, self-check + build ok. --- src/main/debrid.ts | 70 +++++++++++++++++++++++++++++++++++--- tests/debrid.test.ts | 80 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/main/debrid.ts b/src/main/debrid.ts index d07be98..4699e52 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -298,6 +298,22 @@ export const MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART = 3; let megaDebridRotationCursor = 0; +const MEGA_DEBRID_SLOW_ACCOUNT_FLOOR_MS = 6000; +const MEGA_DEBRID_SLOW_ACCOUNT_FACTOR = 3; +const megaDebridAccountLatencyEma = new Map(); + +function recordMegaDebridUnrestrictLatency(cooldownKey: string, elapsedMs: number): void { + if (!Number.isFinite(elapsedMs) || elapsedMs < 0) { + return; + } + const prev = megaDebridAccountLatencyEma.get(cooldownKey); + megaDebridAccountLatencyEma.set(cooldownKey, prev === undefined ? elapsedMs : prev * 0.7 + elapsedMs * 0.3); +} + +export function primeMegaDebridLatencyForTests(cooldownKey: string, emaMs: number): void { + megaDebridAccountLatencyEma.set(cooldownKey, emaMs); +} + export function recordMegaDebridEmptyResponseStreak(accountId: string): number { const streak = (megaDebridEmptyResponseStreaks.get(accountId) || 0) + 1; megaDebridEmptyResponseStreaks.set(accountId, streak); @@ -312,6 +328,7 @@ export function resetMegaDebridRuntimeStateForTests(): void { megaDebridAccountCooldowns.clear(); megaDebridEmptyResponseStreaks.clear(); megaDebridRotationCursor = 0; + megaDebridAccountLatencyEma.clear(); } export function pruneExpiredMegaDebridRuntimeState(now = Date.now()): number { @@ -1915,9 +1932,48 @@ class MegaDebridClient { // wurde nie auch nur angeschaut. Der Cursor laesst jede Aufloesung beim Account // NACH dem zuletzt getesteten starten, alle Skip-/Cooldown-Checks bleiben. const startOffset = ((megaDebridRotationCursor % accounts.length) + accounts.length) % accounts.length; + const rotationOrder: { account: MegaDebridAccountEntry; idx: number }[] = []; for (let step = 0; step < accounts.length; step += 1) { const idx = (startOffset + step) % accounts.length; - const account = accounts[idx]; + rotationOrder.push({ account: accounts[idx], idx }); + } + + // Latenz-Schutz fuer die Mega-Web-Single-Flight-Queue: dort laufen ALLE + // Umwandlungen seriell, ein nachweislich langsamer Account bremst also + // jeden nachfolgenden Link aus. Accounts mit deutlich erhoehtem Erfolgs-EMA + // wandern ans Ende der Reihenfolge (= Failover-Reserve) statt in der + // gleichmaessigen Rotation mitzulaufen; erholt sich ihr EMA, rotieren sie + // automatisch wieder mit. Ungemessene Accounts gelten als gesund. + let bestEma = Infinity; + let anyUnmeasuredUsable = false; + for (const entry of rotationOrder) { + if (isMegaDebridAccountDisabled(settings, entry.account.id)) continue; + if (isMegaDebridAccountDailyLimitReached(settings, entry.account.id)) continue; + if (getMegaDebridAccountCooldownState(`${entry.account.id}:${mode}`)) continue; + const ema = megaDebridAccountLatencyEma.get(`${entry.account.id}:${mode}`); + if (ema === undefined) { + anyUnmeasuredUsable = true; + continue; + } + if (ema < bestEma) bestEma = ema; + } + const slowThreshold = Math.max( + MEGA_DEBRID_SLOW_ACCOUNT_FLOOR_MS, + anyUnmeasuredUsable ? 0 : (Number.isFinite(bestEma) ? MEGA_DEBRID_SLOW_ACCOUNT_FACTOR * bestEma : Infinity) + ); + const isSlowEntry = (entry: { account: MegaDebridAccountEntry }): boolean => { + const ema = megaDebridAccountLatencyEma.get(`${entry.account.id}:${mode}`); + return ema !== undefined && ema > slowThreshold; + }; + const orderedEntries = [ + ...rotationOrder.filter((entry) => !isSlowEntry(entry)), + ...rotationOrder.filter((entry) => isSlowEntry(entry)) + ]; + + for (let orderPos = 0; orderPos < orderedEntries.length; orderPos += 1) { + const entry = orderedEntries[orderPos]; + const account = entry.account; + const idx = entry.idx; const accountLabel = ` (${account.label}/${totalAccounts}, ${account.maskedLogin})`; const rotationLabel = `${account.label}/${totalAccounts} (${account.maskedLogin})`; @@ -1955,8 +2011,13 @@ class MegaDebridClient { continue; } + const emaForLog = megaDebridAccountLatencyEma.get(cooldownKey); logger.info(`Mega-Debrid${accountLabel}: TESTE Account fuer Link-Generierung...`); - logAccountRotation("INFO", providerName, rotationLabel, "TEST", { link: linkShort }); + logAccountRotation("INFO", providerName, rotationLabel, "TEST", { + link: linkShort, + emaMs: emaForLog !== undefined ? Math.round(emaForLog) : undefined, + slow: isSlowEntry(entry) ? true : undefined + }); const testStartedAt = Date.now(); usableAccountSeen = true; @@ -1967,6 +2028,7 @@ class MegaDebridClient { clearMegaDebridAccountCooldownState(cooldownKey); clearMegaDebridEmptyResponseStreak(cooldownKey); const elapsedMs = Date.now() - testStartedAt; + recordMegaDebridUnrestrictLatency(cooldownKey, elapsedMs); logger.info(`Mega-Debrid${accountLabel}: Unrestrict OK nach ${elapsedMs}ms -> ${result.fileName || "?"}`); logAccountRotation("INFO", providerName, rotationLabel, "OK", { elapsedMs, @@ -2039,8 +2101,8 @@ class MegaDebridClient { ? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s` : ""; let nextLabel = "ENDE"; - for (let nextStep = step + 1; nextStep < accounts.length; nextStep += 1) { - const nextAcc = accounts[(startOffset + nextStep) % accounts.length]; + for (let nextPos = orderPos + 1; nextPos < orderedEntries.length; nextPos += 1) { + const nextAcc = orderedEntries[nextPos].account; if (!isMegaDebridAccountDisabled(settings, nextAcc.id) && !isMegaDebridAccountDailyLimitReached(settings, nextAcc.id) && !getMegaDebridAccountCooldownState(`${nextAcc.id}:${mode}`)) { nextLabel = `${nextAcc.label}/${totalAccounts} (${nextAcc.maskedLogin})`; break; diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 8c2cda6..cacf84d 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -3,7 +3,7 @@ import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; -import { clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; +import { clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, normalizeResolvedFilename, primeMegaDebridLatencyForTests, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; const originalFetch = globalThis.fetch; @@ -1463,6 +1463,84 @@ describe("debrid service", () => { ]); }, 30000); + it("depriorisiert einen nachweislich langsamen Account in der Rotation (Single-Flight-Schutz)", async () => { + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user1", + megaPassword: "pass1", + megaCredentials: "user1:pass1\nuser2:pass2", + megaDebridPreferApi: false, + providerOrder: [] as const, + providerPrimary: "megadebrid" as const, + providerSecondary: "none" as const, + providerTertiary: "none" as const, + autoProviderFallback: false + }; + + globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch; + + const megaWeb = vi.fn(async () => ({ + fileName: "ok.rar", + directUrl: "https://mega-web.example/ok.rar", + fileSize: null, + retriesUsed: 0 + })); + + primeMegaDebridLatencyForTests(`${getMegaDebridAccountId("user1")}:web`, 15000); + primeMegaDebridLatencyForTests(`${getMegaDebridAccountId("user2")}:web`, 800); + + const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); + const usedIds: (string | undefined)[] = []; + for (let i = 0; i < 2; i += 1) { + const result = await service.unrestrictLink(`https://rapidgator.net/file/slow-${i}`); + usedIds.push((result as { sourceAccountId?: string }).sourceAccountId); + } + + expect(usedIds).toEqual([ + getMegaDebridAccountId("user2"), + getMegaDebridAccountId("user2") + ]); + }, 30000); + + it("nutzt den langsamen Account weiterhin, wenn die schnellen gesperrt sind (Failover-Reserve)", async () => { + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user1", + megaPassword: "pass1", + megaCredentials: "user1:pass1\nuser2:pass2", + megaDebridPreferApi: false, + providerOrder: [] as const, + providerPrimary: "megadebrid" as const, + providerSecondary: "none" as const, + providerTertiary: "none" as const, + autoProviderFallback: false + }; + + globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch; + + const megaWeb = vi.fn(async () => ({ + fileName: "ok.rar", + directUrl: "https://mega-web.example/ok.rar", + fileSize: null, + retriesUsed: 0 + })); + + primeMegaDebridLatencyForTests(`${getMegaDebridAccountId("user1")}:web`, 15000); + primeMegaDebridLatencyForTests(`${getMegaDebridAccountId("user2")}:web`, 800); + primeMegaDebridUntilRestartForTests(`${getMegaDebridAccountId("user2")}:web`); + + const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); + const result = await service.unrestrictLink("https://rapidgator.net/file/slow-fallback"); + + expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user1")); + }, 30000); + it("rotates to the next Mega-Debrid account when one hits its daily limit (error-based)", async () => { const settings = { ...defaultSettings(),