Mega-Debrid: schnelle API-Aufloesung darf langsame Web-Aufloesung ueberlappen (Single-Account-Durchsatz)

Bei nur einem Mega-Debrid-Account war die Link-Aufloesung global seriell:
getSerializedValidatingLimit = max(1, nutzbare Accounts) = 1. Da ~die Haelfte
der Rapidgator-Links von der Mega-API faelschlich als "Fichier supprimé"
abgelehnt wird (bestaetigt: die offizielle Mega-Debrid-Download-Station-
Integration nutzt dieselbe getLink-API voellig ohne Fallback), fallen sie auf
den Web-Pfad, der pro Account zwingend single-flight ist (eine Web-Session =
ein Request). Eine langsame/haengende Web-Aufloesung (live 11s gesehen, im
Extremfall bis zum 60s-Timeout) hielt so den einzigen Aufloesungs-Slot und
liess die Download-Slots leerlaufen (live gemessen: active faellt von 8 auf 2,
Tempo von ~183 auf 71 MB/s, obwohl hunderte Items warteten).

Fix: der Scheduler erlaubt jetzt pro Account EINE zusaetzliche gleichzeitige
Aufloesung, solange bereits eine im (langsamen) Web-Pfad steckt - so zieht eine
schnelle API-Aufloesung (~0.6s) an einer haengenden Web-Aufloesung vorbei statt
dahinter zu warten. Es entstehen NIE zwei gleichzeitige API-Aufrufe pro
Account: die Aufweitung greift nur, wenn megaDebridInFlight im Web-Modus aktiv
ist (der erste also nicht mehr in der API-Phase), und der synchron in startItem
gesetzte validating-Status begrenzt die Gesamtzahl race-frei auf 1 API + 1 Web
pro Account. Skaliert mit mehr Accounts auf N+N - der sauberste Hebel bleibt,
weitere Accounts hinzuzufuegen (parallele Web-Queues).

getMegaDebridInFlightCountForMode liest die bestehenden :web-Zaehler;
shouldDelayStartForItem nutzt sie nur zum Aufweiten der Obergrenze, nie zum
Verschaerfen.

Tests: 4 Faelle (Erststart frei / kein zweiter API waehrend API-Phase /
Overlap sobald Web-Phase aktiv / Deckel bei 1 API + 1 Web).
This commit is contained in:
Sucukdeluxe 2026-06-22 02:37:03 +02:00
parent ce573fe7b6
commit 8e0ae77aee
3 changed files with 107 additions and 3 deletions

View File

@ -339,6 +339,25 @@ export function resetMegaDebridRuntimeStateForTests(): void {
megaDebridInFlight.clear();
}
export function getMegaDebridInFlightCountForMode(mode: "api" | "web"): number {
const suffix = `:${mode}`;
let total = 0;
for (const [key, count] of megaDebridInFlight) {
if (key.endsWith(suffix)) {
total += count;
}
}
return total;
}
export function primeMegaDebridInFlightForTests(key: string, count: number): void {
if (count <= 0) {
megaDebridInFlight.delete(key);
return;
}
megaDebridInFlight.set(key, count);
}
export function pruneExpiredMegaDebridRuntimeState(now = Date.now()): number {
let removed = 0;
const grace = 60 * 60 * 1000;

View File

@ -52,7 +52,7 @@ function releaseTlsSkip(): void {
}
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
import { validateFileAgainstManifest } from "./integrity";
import { classifyDiskError } from "./fs-error";
@ -8212,7 +8212,13 @@ export class DownloadManager extends EventEmitter {
const provider = resolveMegaDebridProvider(this.settings, this.getExpectedProviderForItem(item));
const serializedValidatingLimit = this.getSerializedValidatingLimit(provider);
if (provider && Number.isFinite(serializedValidatingLimit) && serializedValidatingLimit < Number.MAX_SAFE_INTEGER) {
return this.getProviderValidatingTaskCount(provider, item.id) >= serializedValidatingLimit;
const validating = this.getProviderValidatingTaskCount(provider, item.id);
if (provider === "megadebrid-api") {
const webInFlight = getMegaDebridInFlightCountForMode("web");
const overlapAllowance = Math.min(serializedValidatingLimit, webInFlight);
return validating >= serializedValidatingLimit + overlapAllowance;
}
return validating >= serializedValidatingLimit;
}
if (provider !== "alldebrid") {
return false;

View File

@ -14,7 +14,7 @@ import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
import { createStoragePaths, emptySession } from "../src/main/storage";
import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests, primeMegaDebridInFlightForTests } from "../src/main/debrid";
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
import { UnrestrictedLink } from "../src/main/realdebrid";
@ -12362,3 +12362,82 @@ describe("start conflict guard + selective resume", () => {
manager.stop();
});
});
describe("mega-debrid api/web resolution overlap gate", () => {
function megaApiSettings(root: string): any {
return {
...defaultSettings(),
megaLogin: "u", megaPassword: "p", megaCredentials: "u:p",
megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: true,
outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract")
};
}
function megaItem(id: string, status: string): any {
return {
id, packageId: "pkg", url: `https://rapidgator.net/file/${id}`, provider: "megadebrid-api",
status, retries: 0, speedBps: 0, downloadedBytes: 0, totalBytes: null, progressPercent: 0,
fileName: `${id}.rar`, targetPath: "", resumable: true, attempts: 0, lastError: "", fullStatus: "",
createdAt: Date.now(), updatedAt: Date.now()
};
}
function addValidating(manager: DownloadManager, session: any, ids: string[]): void {
for (const id of ids) {
session.items[id] = megaItem(id, "validating");
(manager as any).activeTasks.set(id, {
itemId: id, packageId: "pkg", abortController: new AbortController(), abortReason: "none",
resumable: true, nonResumableCounted: false, blockedOnDiskWrite: false, blockedOnDiskSince: 0
});
}
}
function buildManager(root: string, session: any): DownloadManager {
return new DownloadManager(megaApiSettings(root), session, createStoragePaths(path.join(root, "state")));
}
it("lets the first mega resolve start when nothing is in flight", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-overlap-0-"));
tempDirs.push(root);
const session = emptySession();
const candidate = megaItem("cand", "queued");
session.items["cand"] = candidate;
const manager = buildManager(root, session);
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(false);
});
it("serializes a second API resolve while the first is still in its API phase (no concurrent API)", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-overlap-1-"));
tempDirs.push(root);
const session = emptySession();
const candidate = megaItem("cand", "queued");
session.items["cand"] = candidate;
const manager = buildManager(root, session);
addValidating(manager, session, ["a"]);
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(true);
});
it("allows one API resolve to overlap once the first has moved to its web phase", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-overlap-2-"));
tempDirs.push(root);
const session = emptySession();
const candidate = megaItem("cand", "queued");
session.items["cand"] = candidate;
const manager = buildManager(root, session);
addValidating(manager, session, ["a"]);
primeMegaDebridInFlightForTests("acc:web", 1);
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(false);
});
it("caps the overlap at one API plus one web (no third concurrent resolve)", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-overlap-3-"));
tempDirs.push(root);
const session = emptySession();
const candidate = megaItem("cand", "queued");
session.items["cand"] = candidate;
const manager = buildManager(root, session);
addValidating(manager, session, ["a", "b"]);
primeMegaDebridInFlightForTests("acc:web", 1);
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(true);
});
});