From 63f87a0310e7e681b437898009becc869fb8eeb8 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 19 Apr 2026 23:13:25 +0200 Subject: [PATCH] fix(rotation): concurrent jobs now reuse the override instead of failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple jobs run in parallel on the same hoster and the primary account starts failing, the first job marks it failed + triggers rotation. The second job's retries then also exhaust on the same (already-failed) primary — but the old while-condition `!_failedAccounts.has(...)` short-circuited the whole rotation loop for anything already marked, so the second job went straight to final-error even though a resolved override was sitting right there. Now the loop always checks for an available override; it only skips the mark-failed + emit step if the account was already marked by a concurrent job. Fixed visible symptom: first job rotates A→B, every other job in the same batch that hit A got final-error instead of also switching to B. Also extended fast-fail patterns to include 429 (Too many requests), CSRF-Token / 'Bist du eingeloggt' — both were showing up as the primary failure mode in real uploads and were wasting 5 retries each. --- lib/upload-manager.js | 52 +++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/upload-manager.js b/lib/upload-manager.js index eb376a6..68eac54 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -72,14 +72,20 @@ class UploadManager extends EventEmitter { /limit (reached|exceeded|überschritten)/i, /rate[- ]?limit/i, /too many requests/i, - /\b(401|403)\b/, + /\b(401|403|429)\b/, /Falscher (User|Username|Passwort)/i, /Incorrect (Login|Password)/i, /invalid (credentials|api[- ]?key|token|session)/i, /(account|user) (banned|suspended|disabled|gesperrt)/i, /not authorized/i, /forbidden/i, - /session (expired|abgelaufen)/i + /session (expired|abgelaufen)/i, + // Session/CSRF hints — the account's server session went stale, which + // no amount of retrying will fix. Re-login happens on the next account. + /CSRF[- ]?Token nicht gefunden/i, + /CSRF[- ]?token not found/i, + /Bist du eingeloggt/i, + /not logged in/i ]; return PATTERNS.some(p => p.test(m)); } @@ -524,23 +530,34 @@ class UploadManager extends EventEmitter { return; } - // Account rotation: mark the current account failed, wait for main to - // resolve the next fallback, then retry. Loop so A → B → C → ... works - // for hosters with 3+ accounts (the old code only did one level: A → B - // and stopped, even if C would have worked). + // Account rotation: mark the current account failed (if not already), + // wait for main to resolve the next fallback, then retry. Loops so + // A → B → C → ... works for hosters with 3+ accounts. + // + // CRITICAL: we must ALWAYS check for an existing override, even if this + // account is already in _failedAccounts (e.g. another concurrent job + // already marked it failed). Otherwise the second job falls straight + // through to final-error instead of using the already-resolved fallback. this._rotLog('retries-exhausted', { hoster: task.hoster, fileName, accountId: task.accountId, lastError: lastError ? lastError.message : null }); - while (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) { + while (task.accountId) { if (signal.aborted || this.stopAfterActive) break; - this._failedAccounts.set(task.hoster + ':' + task.accountId, true); - this._rotLog('mark-failed', { - hoster: task.hoster, fileName, accountId: task.accountId, - lastError: lastError ? lastError.message : null - }); - this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId }); - await this._sleep(800, signal); + const alreadyMarked = this._failedAccounts.has(task.hoster + ':' + task.accountId); + if (!alreadyMarked) { + this._failedAccounts.set(task.hoster + ':' + task.accountId, true); + this._rotLog('mark-failed', { + hoster: task.hoster, fileName, accountId: task.accountId, + lastError: lastError ? lastError.message : null + }); + this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId }); + await this._sleep(800, signal); + } else { + this._rotLog('already-marked', { + hoster: task.hoster, fileName, accountId: task.accountId + }); + } const override = this._accountOverrides.get(task.hoster); if (!override) { this._rotLog('rotation-end', { @@ -556,6 +573,13 @@ class UploadManager extends EventEmitter { }); break; } + if (override.id === task.accountId) { + this._rotLog('rotation-end', { + hoster: task.hoster, fileName, reason: 'override-same-as-current', + lastFailedAccountId: task.accountId + }); + break; + } // Switch to fallback account and retry this file this._rotLog('rotate', { hoster: task.hoster, fileName,