Mega-Debrid: Link-Umwandlung parallel ueber mehrere Accounts (per-Account-Queue + Tiefen-Routing)
User-Wunsch: die Links eines Pakets parallel umwandeln statt seriell (single-flight),
damit sich die Download-Slots schneller fuellen. Gewaehlt: parallel ueber mehrere
Accounts (ein Login je Account laeuft parallel), NIE zwei gleichzeitig auf demselben
Account (Mega-Debrid-Sperr-Risiko).
- MegaWebFallback: globale Einzel-Warteschlange (this.queue) -> Warteschlange PRO Account
(this.queues: Map<login, Promise>). Gleicher Account serialisiert (kein Doppel-Login,
kein Hammern), verschiedene Accounts parallel. Key vor runExclusive berechnet.
- debrid.ts unrestrictWithAccounts: megaDebridInFlight zaehlt die LAUFENDE Tiefe pro
Account (Map<`${id}:${mode}`, number>). Kandidaten werden nach aufsteigender Tiefe
stabil sortiert (cursorOrder als Gleichstand-Tiebreak): gleichzeitige Aufloesungen
greifen den am wenigsten belegten Account -> auch bei mehr Links als Accounts
gleichmaessige Verteilung (4 Accounts, 8 parallel -> 2 je Account), statt sich hinter
dem Cursor-Account zu stauen. Sequenziell (alles Tiefe 0) bleibt es klebrig beim warmen
Account. add/inc vor dem try, dec/cleanup im finally (kein Leak).
- classifyAccountFailure: "Queue-Timeout" (lokaler Eigen-Stau) gibt jetzt cooldownMs:0 —
der warme Account wird nicht mehr faelschlich fuer Eigen-Stau mit Cooldown bestraft.
Vor Release adversarial per Multi-Agent-Workflow geprueft (4 Lenses + Verify). Der Review
fand genau die Ueberzahl-Stau-Schwaeche (binaeres belegt/frei staute >Accounts-Links hinter
einem Account) — daraufhin auf Tiefen-Zaehlung umgestellt. Sperr-Risiko strukturell
ausgeschlossen (per-Account-Queue serialisiert unabhaengig vom Set). 4 neue Tests
(gleicher Account 1 Login, verschiedene Accounts parallel, 4 gleichzeitig->4 Accounts,
8/2-Ueberzahl->4/4 ausgewogen). 813 Tests, tsc=6, self-check + build ok.
This commit is contained in:
+36
-2
@@ -306,6 +306,13 @@ let megaDebridStickyCount = 0;
|
||||
// naechsten gewechselt. So bleibt es schnell UND ueber die Zeit kommen alle dran.
|
||||
export const MEGA_DEBRID_STICKY_LINKS = 25;
|
||||
|
||||
// Wie viele Umwandlungen pro Account (key `${id}:${mode}`) GERADE laufen/anstehen. Gleichzeitige
|
||||
// Aufloesungen waehlen den am WENIGSTEN belegten Account → verschiedene Accounts wandeln parallel
|
||||
// um (je eigene Queue in MegaWebFallback), und auch bei mehr gleichzeitigen Links als Accounts
|
||||
// verteilt es sich gleichmaessig statt sich hinter einem Account zu stauen. Tiefe (nicht nur
|
||||
// belegt/frei), damit ein frueh fertiger Erst-Job einen noch laufenden Folge-Job nicht "frei" meldet.
|
||||
const megaDebridInFlight = new Map<string, number>();
|
||||
|
||||
export function recordMegaDebridEmptyResponseStreak(accountId: string): number {
|
||||
const streak = (megaDebridEmptyResponseStreaks.get(accountId) || 0) + 1;
|
||||
megaDebridEmptyResponseStreaks.set(accountId, streak);
|
||||
@@ -321,6 +328,7 @@ export function resetMegaDebridRuntimeStateForTests(): void {
|
||||
megaDebridEmptyResponseStreaks.clear();
|
||||
megaDebridRotationCursor = 0;
|
||||
megaDebridStickyCount = 0;
|
||||
megaDebridInFlight.clear();
|
||||
}
|
||||
|
||||
export function pruneExpiredMegaDebridRuntimeState(now = Date.now()): number {
|
||||
@@ -1923,11 +1931,22 @@ class MegaDebridClient {
|
||||
// Schwung erfolgreicher Umwandlungen (MEGA_DEBRID_STICKY_LINKS) — sodass aufeinander
|
||||
// folgende Links auf demselben warmen Account laufen statt jeweils neu einzuloggen.
|
||||
const startOffset = ((megaDebridRotationCursor % accounts.length) + accounts.length) % accounts.length;
|
||||
const orderedEntries: { account: MegaDebridAccountEntry; idx: number }[] = [];
|
||||
const cursorOrder: { account: MegaDebridAccountEntry; idx: number }[] = [];
|
||||
for (let step = 0; step < accounts.length; step += 1) {
|
||||
const idx = (startOffset + step) % accounts.length;
|
||||
orderedEntries.push({ account: accounts[idx], idx });
|
||||
cursorOrder.push({ account: accounts[idx], idx });
|
||||
}
|
||||
// Parallel ueber mehrere Accounts: nach AKTUELLER Auslastung (in-flight-Tiefe) sortieren —
|
||||
// am wenigsten belegter Account zuerst, cursorOrder als stabiler Gleichstand-Tiebreak. So
|
||||
// verteilen sich auch MEHR gleichzeitige Aufloesungen als Accounts gleichmaessig (statt sich
|
||||
// alle hinter dem Cursor-Account zu stauen). Nichts in-flight (sequenziell) => alle Tiefe 0 =>
|
||||
// Reihenfolge == cursorOrder => bleibt klebrig beim warmen Account.
|
||||
const inFlightDepth = (entry: { account: MegaDebridAccountEntry }): number =>
|
||||
megaDebridInFlight.get(`${entry.account.id}:${mode}`) ?? 0;
|
||||
const orderedEntries = cursorOrder
|
||||
.map((entry, position) => ({ entry, position }))
|
||||
.sort((a, b) => (inFlightDepth(a.entry) - inFlightDepth(b.entry)) || (a.position - b.position))
|
||||
.map((wrapped) => wrapped.entry);
|
||||
|
||||
for (let orderPos = 0; orderPos < orderedEntries.length; orderPos += 1) {
|
||||
const entry = orderedEntries[orderPos];
|
||||
@@ -1977,6 +1996,7 @@ class MegaDebridClient {
|
||||
const testStartedAt = Date.now();
|
||||
|
||||
usableAccountSeen = true;
|
||||
megaDebridInFlight.set(cooldownKey, (megaDebridInFlight.get(cooldownKey) ?? 0) + 1);
|
||||
try {
|
||||
const client = new MegaDebridClient(account.login, account.password, mode, allowApiFallback, megaWebUnrestrict);
|
||||
const result = await client.unrestrictLink(link, signal);
|
||||
@@ -2078,6 +2098,13 @@ class MegaDebridClient {
|
||||
next: nextLabel,
|
||||
link: linkShort
|
||||
});
|
||||
} finally {
|
||||
const remainingInFlight = (megaDebridInFlight.get(cooldownKey) ?? 1) - 1;
|
||||
if (remainingInFlight <= 0) {
|
||||
megaDebridInFlight.delete(cooldownKey);
|
||||
} else {
|
||||
megaDebridInFlight.set(cooldownKey, remainingInFlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2154,6 +2181,13 @@ class MegaDebridClient {
|
||||
};
|
||||
}
|
||||
|
||||
if (/queue.?timeout/i.test(errorText)) {
|
||||
// Lokaler Stau (die Queue dieses Accounts war zu lange belegt) — KEIN Signal fuer einen
|
||||
// ungesunden Account. Kein Cooldown, sonst wuerde der warme Account fuer Eigen-Stau bestraft;
|
||||
// der Link wird einfach erneut versucht und rotiert dann natuerlich weiter.
|
||||
return { fatal: false, cooldownMs: 0, message: errorText, category: "temporary" };
|
||||
}
|
||||
|
||||
if (isRetryableErrorText(errorText) || /timeout|network|fetch|socket/i.test(errorText)) {
|
||||
return {
|
||||
fatal: false,
|
||||
|
||||
@@ -217,7 +217,11 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
|
||||
}
|
||||
|
||||
export class MegaWebFallback {
|
||||
private queue: Promise<unknown> = Promise.resolve();
|
||||
// Pro Account eine eigene Warteschlange: Umwandlungen auf DEMSELBEN Account laufen
|
||||
// seriell (kein Doppel-Login, kein Hammern eines einzelnen Accounts), verschiedene
|
||||
// Accounts laufen parallel. So koennen die Links eines Pakets ueber mehrere Accounts
|
||||
// gleichzeitig umgewandelt werden statt global eine nach der anderen.
|
||||
private queues = new Map<string, Promise<unknown>>();
|
||||
|
||||
private getCredentials: () => MegaCredentials;
|
||||
|
||||
@@ -233,15 +237,15 @@ export class MegaWebFallback {
|
||||
account?: { login: string; password: string }
|
||||
): Promise<UnrestrictedLink | null> {
|
||||
const overallSignal = withTimeoutSignal(signal, 180000);
|
||||
const creds = (account && account.login.trim() && account.password.trim())
|
||||
? account
|
||||
: this.getCredentials();
|
||||
if (!creds.login.trim() || !creds.password.trim()) {
|
||||
return null;
|
||||
}
|
||||
const key = creds.login.trim().toLowerCase();
|
||||
return this.runExclusive(async () => {
|
||||
throwIfAborted(overallSignal);
|
||||
const creds = (account && account.login.trim() && account.password.trim())
|
||||
? account
|
||||
: this.getCredentials();
|
||||
if (!creds.login.trim() || !creds.password.trim()) {
|
||||
return null;
|
||||
}
|
||||
const key = creds.login.trim().toLowerCase();
|
||||
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
|
||||
|
||||
let generated = await this.generate(link, cookie, overallSignal);
|
||||
@@ -259,7 +263,7 @@ export class MegaWebFallback {
|
||||
fileSize: null,
|
||||
retriesUsed: 0
|
||||
};
|
||||
}, overallSignal);
|
||||
}, key, overallSignal);
|
||||
}
|
||||
|
||||
private async ensureSession(key: string, login: string, password: string, signal?: AbortSignal): Promise<string> {
|
||||
@@ -276,7 +280,7 @@ export class MegaWebFallback {
|
||||
this.sessions.clear();
|
||||
}
|
||||
|
||||
private async runExclusive<T>(job: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
||||
const queuedAt = Date.now();
|
||||
const QUEUE_WAIT_TIMEOUT_MS = 90000;
|
||||
const guardedJob = async (): Promise<T> => {
|
||||
@@ -287,8 +291,9 @@ export class MegaWebFallback {
|
||||
}
|
||||
return job();
|
||||
};
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
const prev = this.queues.get(key) ?? Promise.resolve();
|
||||
const run = prev.then(guardedJob, guardedJob);
|
||||
this.queues.set(key, run.then(() => undefined, () => undefined));
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user