From 5bd0eaa0b889b16ccd459d5323d9effb21877818 Mon Sep 17 00:00:00 2001 From: Nekohy Date: Thu, 30 Apr 2026 15:29:59 +0800 Subject: [PATCH 01/15] =?UTF-8?q?fix:=E9=AB=98=E5=B9=B6=E5=8F=91=E4=B8=8B5?= =?UTF-8?q?03=E7=AA=97=E5=8F=A3=E5=85=B3=E9=97=AD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/BrowserManager.js | 162 +++++++++++++++++++++++++++++++-- src/core/ConnectionRegistry.js | 30 ++++++ 2 files changed, 184 insertions(+), 8 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index b976c68..57dd1ed 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -55,6 +55,8 @@ class BrowserManager { // ConnectionRegistry reference (set after construction to avoid circular dependency) this.connectionRegistry = null; + this._onAuthQueuesDrained = null; + this.pendingContextClosures = new Map(); // Background wakeup service status (instance-level, tracks this.page) // Prevents multiple BackgroundWakeup instances from running simultaneously @@ -148,7 +150,20 @@ class BrowserManager { * @param {ConnectionRegistry} connectionRegistry - The ConnectionRegistry instance */ setConnectionRegistry(connectionRegistry) { + if (this.connectionRegistry && this._onAuthQueuesDrained) { + this.connectionRegistry.off("authQueuesDrained", this._onAuthQueuesDrained); + } this.connectionRegistry = connectionRegistry; + if (this.connectionRegistry) { + this._onAuthQueuesDrained = authIndex => { + this._closePendingContextIfIdle(authIndex).catch(error => { + this.logger.error( + `[ContextPool] Failed to close pending context #${authIndex} after queue drain: ${error.message}` + ); + }); + }; + this.connectionRegistry.on("authQueuesDrained", this._onAuthQueuesDrained); + } } /** @@ -1532,6 +1547,105 @@ class BrowserManager { }); } + _getActiveQueueCountForAuth(authIndex) { + if (!this.connectionRegistry?.getMessageQueueCountForAuth) { + return 0; + } + return this.connectionRegistry.getMessageQueueCountForAuth(authIndex); + } + + _cancelPendingContextClosure(authIndex, reason = "closure_cancelled") { + if (!this.pendingContextClosures.has(authIndex)) { + return false; + } + this.pendingContextClosures.delete(authIndex); + this.logger.info(`[ContextPool] Cancelled pending close for context #${authIndex} (${reason}).`); + return true; + } + + _scheduleContextClosureWhenIdle(authIndex, reason, activeQueueCount) { + if (!this.contexts.has(authIndex)) { + return false; + } + const existingReason = this.pendingContextClosures.get(authIndex); + if (existingReason) { + this.logger.debug( + `[ContextPool] Context #${authIndex} is already pending close (${existingReason}), active queues: ${activeQueueCount}` + ); + return false; + } + this.pendingContextClosures.set(authIndex, reason); + this.logger.info( + `[ContextPool] Deferring close for busy context #${authIndex} (${activeQueueCount} active queue(s), reason: ${reason})` + ); + return false; + } + + async _closeContextForPoolIfPossible(authIndex, reason) { + const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); + if (activeQueueCount > 0) { + return this._scheduleContextClosureWhenIdle(authIndex, reason, activeQueueCount); + } + + this.pendingContextClosures.delete(authIndex); + await this.closeContext(authIndex); + return true; + } + + async _closePendingContextIfIdle(authIndex) { + if (!this.pendingContextClosures.has(authIndex)) { + return false; + } + if (authIndex === this._currentAuthIndex) { + this.logger.debug( + `[ContextPool] Skipping pending close for context #${authIndex} because it is active again as current.` + ); + return false; + } + if (!this.contexts.has(authIndex)) { + this.pendingContextClosures.delete(authIndex); + return false; + } + + const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); + if (activeQueueCount > 0) { + this.logger.debug( + `[ContextPool] Pending close for context #${authIndex} is still waiting on ${activeQueueCount} active queue(s).` + ); + return false; + } + + const pendingReason = this.pendingContextClosures.get(authIndex); + this.pendingContextClosures.delete(authIndex); + this.logger.info( + `[ContextPool] Closing deferred context #${authIndex} now that all queues are drained (reason: ${pendingReason}).` + ); + await this.closeContext(authIndex); + return true; + } + + async _flushPendingContextClosures() { + for (const authIndex of [...this.pendingContextClosures.keys()]) { + await this._closePendingContextIfIdle(authIndex); + } + } + + _prioritizeContextsForRemoval(indices) { + const idle = []; + const busy = []; + + for (const authIndex of indices) { + const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); + if (activeQueueCount > 0) { + busy.push({ activeQueueCount, authIndex }); + } else { + idle.push(authIndex); + } + } + + return { busy, idle }; + } + /** * Internal method to execute the actual preload task * @private @@ -1754,15 +1868,32 @@ class BrowserManager { } } - // Remove contexts according to priority until we have enough space - const toRemove = removalPriority.slice(0, removeCount); + const { busy, idle } = this._prioritizeContextsForRemoval(removalPriority); + const toRemoveNow = idle.slice(0, removeCount); + const toDefer = busy.slice(0, Math.max(0, removeCount - toRemoveNow.length)); this.logger.info( - `[ContextPool] Pre-cleanup: removing ${toRemove.length} contexts before switch to #${targetAuthIndex}: [${toRemove}] (${this.contexts.size} ready + ${this.initializingContexts.size} initializing)` + `[ContextPool] Pre-cleanup before switch to #${targetAuthIndex}: immediate=[${toRemoveNow}], deferred=[${toDefer.map( + entry => `${entry.authIndex}:${entry.activeQueueCount}` + )}] (${this.contexts.size} ready + ${this.initializingContexts.size} initializing)` ); - for (const idx of toRemove) { - await this.closeContext(idx); + for (const idx of toRemoveNow) { + await this._closeContextForPoolIfPossible(idx, "pre_cleanup_for_switch"); + } + + for (const entry of toDefer) { + this._scheduleContextClosureWhenIdle( + entry.authIndex, + "pre_cleanup_for_switch", + entry.activeQueueCount + ); + } + + if (toDefer.length > 0) { + this.logger.warn( + `[ContextPool] Allowing temporary MAX_CONTEXTS overflow while busy contexts drain before switch to #${targetAuthIndex}.` + ); } } @@ -1835,12 +1966,20 @@ class BrowserManager { // If a foreground task is running, _executePreloadTask will skip it (line 1382) const candidates = ordered.filter(idx => !activeContexts.has(idx)); + const { busy, idle } = this._prioritizeContextsForRemoval(toRemove); + this.logger.info( - `[ContextPool] Rebalance: targets=[${[...targets]}], remove=[${toRemove}], candidates=[${candidates}]` + `[ContextPool] Rebalance: targets=[${[...targets]}], removeNow=[${idle}], removeDeferred=[${busy.map( + entry => `${entry.authIndex}:${entry.activeQueueCount}` + )}], candidates=[${candidates}]` ); - for (const idx of toRemove) { - await this.closeContext(idx); + for (const idx of idle) { + await this._closeContextForPoolIfPossible(idx, "rebalance"); + } + + for (const entry of busy) { + this._scheduleContextClosureWhenIdle(entry.authIndex, "rebalance", entry.activeQueueCount); } // Preload candidates if we have room in the pool @@ -2115,6 +2254,8 @@ class BrowserManager { throw new Error(`Invalid authIndex: ${authIndex}. Must be >= 0.`); } + this._cancelPendingContextClosure(authIndex, "context_reused"); + // [Auth Switch] Save current auth data before switching if (this.browser && this._currentAuthIndex >= 0 && this._currentAuthIndex !== authIndex) { try { @@ -2194,6 +2335,7 @@ class BrowserManager { // Switch to new context this._activateContext(contextData.context, contextData.page, authIndex); + await this._flushPendingContextClosures(); this.logger.info(`✅ [FastSwitch] Switched to account #${authIndex} instantly!`); return; @@ -2249,6 +2391,7 @@ class BrowserManager { const { context, page } = await this._initializeContext(authIndex, false); this._activateContext(context, page, authIndex); + await this._flushPendingContextClosures(); // If this account was marked as expired but login succeeded, restore it if (this.authSource.isExpired(authIndex)) { @@ -2456,6 +2599,8 @@ class BrowserManager { * @param {number} authIndex - The auth index to close */ async closeContext(authIndex) { + this.pendingContextClosures.delete(authIndex); + // If context is being initialized in background, signal abort and wait if (this.initializingContexts.has(authIndex)) { this.logger.info(`[Browser] Context #${authIndex} is being initialized, marking for abort and waiting...`); @@ -2549,6 +2694,7 @@ class BrowserManager { this.contexts.clear(); this.initializingContexts.clear(); this.abortedContexts.clear(); + this.pendingContextClosures.clear(); this._wsInitState.clear(); this.context = null; this.page = null; diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index 5be2107..73082be 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -426,6 +426,7 @@ class ConnectionRegistry extends EventEmitter { // This prevents stale queues from lingering when retrying failed requests const existingEntry = this.messageQueues.get(requestId); if (existingEntry) { + const existingAuthIndex = existingEntry.authIndex; this.logger.debug( `[Registry] Found existing message queue for request ${requestId} (authIndex=${existingEntry.authIndex}), closing it before creating new one` ); @@ -435,6 +436,7 @@ class ConnectionRegistry extends EventEmitter { this.logger.debug(`[Registry] Failed to close existing queue for ${requestId}: ${e.message}`); } this.messageQueues.delete(requestId); + this._emitAuthQueuesDrainedIfNeeded(existingAuthIndex); } const queue = new MessageQueue(); @@ -456,8 +458,10 @@ class ConnectionRegistry extends EventEmitter { removeMessageQueue(requestId, reason = "handler_cleanup") { const entry = this.messageQueues.get(requestId); if (entry) { + const authIndex = entry.authIndex; entry.queue.close(reason); this.messageQueues.delete(requestId); + this._emitAuthQueuesDrainedIfNeeded(authIndex); } } @@ -481,6 +485,21 @@ class ConnectionRegistry extends EventEmitter { return entry ? entry.requestAttemptId || null : null; } + /** + * Get the number of active message queues for a specific account + * @param {number} authIndex - The account index to inspect + * @returns {number} Number of active message queues + */ + getMessageQueueCountForAuth(authIndex) { + let count = 0; + for (const entry of this.messageQueues.values()) { + if (entry.authIndex === authIndex) { + count++; + } + } + return count; + } + /** * Close all message queues belonging to a specific account * @param {number} authIndex - The account whose queues should be closed @@ -505,6 +524,7 @@ class ConnectionRegistry extends EventEmitter { `[Registry] Force closed ${count} pending message queue(s) for account #${authIndex} (reason: ${reason})` ); } + this._emitAuthQueuesDrainedIfNeeded(authIndex); return count; } @@ -548,6 +568,7 @@ class ConnectionRegistry extends EventEmitter { this.logger.debug(`[Registry] Failed to close stale queue for ${requestId}: ${e.message}`); } this.messageQueues.delete(requestId); + this._emitAuthQueuesDrainedIfNeeded(entry.authIndex); cleanedCount++; } } @@ -559,6 +580,15 @@ class ConnectionRegistry extends EventEmitter { return cleanedCount; } + _emitAuthQueuesDrainedIfNeeded(authIndex) { + if (!Number.isInteger(authIndex) || authIndex < 0) { + return; + } + if (this.getMessageQueueCountForAuth(authIndex) === 0) { + this.emit("authQueuesDrained", authIndex); + } + } + /** * Safely close a WebSocket connection with readyState check * @param {WebSocket} ws - The WebSocket to close From f974286c607a67f2a91a97f803f5c42451f97833 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 15:51:12 +0800 Subject: [PATCH 02/15] chore: add context pool rebalance handling and prevent premature context closures --- src/core/BrowserManager.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 57dd1ed..4fa11ad 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -1621,6 +1621,9 @@ class BrowserManager { `[ContextPool] Closing deferred context #${authIndex} now that all queues are drained (reason: ${pendingReason}).` ); await this.closeContext(authIndex); + this.rebalanceContextPool().catch(error => { + this.logger.error(`[ContextPool] Rebalance after deferred close failed: ${error.message}`); + }); return true; } @@ -1883,11 +1886,7 @@ class BrowserManager { } for (const entry of toDefer) { - this._scheduleContextClosureWhenIdle( - entry.authIndex, - "pre_cleanup_for_switch", - entry.activeQueueCount - ); + this._scheduleContextClosureWhenIdle(entry.authIndex, "pre_cleanup_for_switch", entry.activeQueueCount); } if (toDefer.length > 0) { @@ -1927,6 +1926,10 @@ class BrowserManager { targets = new Set(ordered.slice(0, maxContexts)); } + for (const idx of targets) { + this._cancelPendingContextClosure(idx, "rebalance_target"); + } + // Remove contexts not in targets (except current) // Special handling: if current account is a duplicate (old version), also remove its canonical version // BUT only in limited mode - in unlimited mode, keep all contexts From f70af01e2476480e54c7c58ed7e558677ee894d2 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 16:31:06 +0800 Subject: [PATCH 03/15] fix: add system busy state handling to prevent context pool rebalance during high load --- src/core/BrowserManager.js | 23 +++++++++++++++++++++-- src/core/ProxyServerSystem.js | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 4fa11ad..9406c86 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -56,6 +56,7 @@ class BrowserManager { // ConnectionRegistry reference (set after construction to avoid circular dependency) this.connectionRegistry = null; this._onAuthQueuesDrained = null; + this._isSystemBusyProvider = null; this.pendingContextClosures = new Map(); // Background wakeup service status (instance-level, tracks this.page) @@ -166,6 +167,19 @@ class BrowserManager { } } + setSystemBusyProvider(provider) { + this._isSystemBusyProvider = typeof provider === "function" ? provider : null; + } + + _isSystemBusy() { + try { + return this._isSystemBusyProvider?.() === true; + } catch (error) { + this.logger.warn(`[ContextPool] Failed to read system busy state: ${error.message}`); + return false; + } + } + /** * Helper: Check for page errors that require refresh * @returns {Object} Object with error flags @@ -1621,6 +1635,10 @@ class BrowserManager { `[ContextPool] Closing deferred context #${authIndex} now that all queues are drained (reason: ${pendingReason}).` ); await this.closeContext(authIndex); + if (this._isSystemBusy()) { + this.logger.info("[ContextPool] Skipping rebalance after deferred close because system is busy."); + return true; + } this.rebalanceContextPool().catch(error => { this.logger.error(`[ContextPool] Rebalance after deferred close failed: ${error.message}`); }); @@ -1985,8 +2003,9 @@ class BrowserManager { this._scheduleContextClosureWhenIdle(entry.authIndex, "rebalance", entry.activeQueueCount); } - // Preload candidates if we have room in the pool - if (candidates.length > 0 && (isUnlimited || this.contexts.size < maxContexts)) { + // Preload candidates if ready and initializing contexts still leave room in the pool + const poolOccupancy = this.contexts.size + this.initializingContexts.size; + if (candidates.length > 0 && (isUnlimited || poolOccupancy < maxContexts)) { this._preloadBackgroundContexts(candidates, isUnlimited ? 0 : maxContexts); } } diff --git a/src/core/ProxyServerSystem.js b/src/core/ProxyServerSystem.js index 5699a3c..5a34241 100644 --- a/src/core/ProxyServerSystem.js +++ b/src/core/ProxyServerSystem.js @@ -111,6 +111,7 @@ class ProxyServerSystem extends EventEmitter { this.config, this.authSource ); + this.browserManager.setSystemBusyProvider(() => this.requestHandler?.isSystemBusy === true); this.httpServer = null; this.wsServer = null; From 87ef0ac6c1b94f8eac29edbe3498684b4fa31176 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 17:20:47 +0800 Subject: [PATCH 04/15] fix: enhance request handling to support retries on closed accounts during high load --- src/core/RequestHandler.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index db96f8d..c24ead3 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -3271,10 +3271,45 @@ class RequestHandler { // Check the actual closure reason to provide accurate error messages const reason = error.reason || "unknown"; const isClientDisconnect = reason === "client_disconnect"; + const currentAuthIndex = this.currentAuthIndex; + const isClosedAccountRetryable = reason === "context_closed" || reason === "page_closed"; + const canRetryOnCurrentAccount = + !isClientDisconnect && + isClosedAccountRetryable && + retryAttempt < this.maxRetries && + Number.isInteger(currentQueueAuthIndex) && + currentQueueAuthIndex >= 0 && + Number.isInteger(currentAuthIndex) && + currentAuthIndex >= 0 && + currentQueueAuthIndex !== currentAuthIndex && + Boolean(this.connectionRegistry.getConnectionByAuth(currentAuthIndex)); if (isClientDisconnect) { this.logger.warn(`[Request] Message queue closed due to client disconnect, aborting retries.`); lastError = { message: "Connection lost (client disconnect)", status: 503 }; + } else if (canRetryOnCurrentAccount) { + this.logger.warn( + `[Request] Message queue for non-current account #${currentQueueAuthIndex} closed ` + + `(reason: ${reason}); retrying request #${proxyRequest.request_id} on current account #${currentAuthIndex}.` + ); + lastError = { + message: `Queue closed: ${error.message || reason}`, + reason, + status: 503, + }; + this._advanceProxyRequestAttempt(proxyRequest); + currentQueue = this.connectionRegistry.createMessageQueue( + proxyRequest.request_id, + currentAuthIndex, + proxyRequest.request_attempt_id + ); + currentQueueAuthIndex = currentAuthIndex; + if (Number.isInteger(currentQueueAuthIndex) && currentQueueAuthIndex >= 0) { + immediateSwitchTracker.attemptedAuthIndices.add(currentQueueAuthIndex); + } + await new Promise(resolve => setTimeout(resolve, this.retryDelay)); + retryAttempt++; + continue; } else { // Queue closed for other reasons (account_switch, system_reset, etc.) this.logger.warn(`[Request] Message queue closed (reason: ${reason}), aborting retries.`); From b1dc0ac451e39e2a4fb17bdcc5cd586cca05bfe8 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 17:58:35 +0800 Subject: [PATCH 05/15] fix: prevent unnecessary emission of auth queues drained event during high load --- src/core/ConnectionRegistry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index 73082be..2d6dfea 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -436,7 +436,9 @@ class ConnectionRegistry extends EventEmitter { this.logger.debug(`[Registry] Failed to close existing queue for ${requestId}: ${e.message}`); } this.messageQueues.delete(requestId); - this._emitAuthQueuesDrainedIfNeeded(existingAuthIndex); + if (existingAuthIndex !== authIndex) { + this._emitAuthQueuesDrainedIfNeeded(existingAuthIndex); + } } const queue = new MessageQueue(); From 7937f199ee2ff4b98a8f76414dfe198251df80ad Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 18:11:18 +0800 Subject: [PATCH 06/15] fix: update auth index tracking for request retries to handle registered queues correctly --- src/core/RequestHandler.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index c24ead3..8c092db 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -3217,8 +3217,12 @@ class RequestHandler { async _executeRequestWithRetries(proxyRequest, messageQueue) { let lastError = null; let currentQueue = messageQueue; - // Track the authIndex for the current queue to ensure proper cleanup - let currentQueueAuthIndex = this.currentAuthIndex; + const registeredQueueAuthIndex = this.connectionRegistry.getAuthIndexForRequest(proxyRequest.request_id); + // Track the authIndex registered for the current queue, which may differ from the global current account. + let currentQueueAuthIndex = + Number.isInteger(registeredQueueAuthIndex) && registeredQueueAuthIndex >= 0 + ? registeredQueueAuthIndex + : this.currentAuthIndex; let retryAttempt = 1; const immediateSwitchTracker = this._createImmediateSwitchTracker(currentQueueAuthIndex); From 48d082b97211858b8fdc20ac66ad671386e52125 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 18:27:12 +0800 Subject: [PATCH 07/15] fix: optimize connection retrieval logging to reduce unnecessary debug output --- src/core/ConnectionRegistry.js | 10 ++++++++-- src/core/RequestHandler.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index 2d6dfea..e4327c2 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -309,13 +309,19 @@ class ConnectionRegistry extends EventEmitter { getConnectionByAuth(authIndex, log = true) { const connection = this.connectionsByAuth.get(authIndex); - if (connection && log) { + + if (!log) { + return connection; + } + + if (connection) { this.logger.debug(`[Registry] Found WebSocket connection for authIndex=${authIndex}`); - } else if (this.logger.getLevel?.() === "DEBUG") { + } else { this.logger.debug( `[Registry] No WebSocket connection found for authIndex=${authIndex}. Available: [${Array.from(this.connectionsByAuth.keys()).join(", ")}]` ); } + return connection; } diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index 8c092db..153694f 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -3286,7 +3286,7 @@ class RequestHandler { Number.isInteger(currentAuthIndex) && currentAuthIndex >= 0 && currentQueueAuthIndex !== currentAuthIndex && - Boolean(this.connectionRegistry.getConnectionByAuth(currentAuthIndex)); + Boolean(this.connectionRegistry.getConnectionByAuth(currentAuthIndex, false)); if (isClientDisconnect) { this.logger.warn(`[Request] Message queue closed due to client disconnect, aborting retries.`); From 4b6d4df23fe2fe8bb9e920a6bce5b6e4e718aeb0 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 18:42:47 +0800 Subject: [PATCH 08/15] fix: refactor queue handling methods to improve clarity and reduce unnecessary parameters --- src/core/BrowserManager.js | 37 ++++++++++++++++------------------ src/core/ConnectionRegistry.js | 14 ++++--------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 9406c86..3a532b2 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -1561,11 +1561,11 @@ class BrowserManager { }); } - _getActiveQueueCountForAuth(authIndex) { - if (!this.connectionRegistry?.getMessageQueueCountForAuth) { - return 0; + _hasActiveQueueForAuth(authIndex) { + if (this.connectionRegistry?._hasMessageQueueForAuth) { + return this.connectionRegistry._hasMessageQueueForAuth(authIndex); } - return this.connectionRegistry.getMessageQueueCountForAuth(authIndex); + return false; } _cancelPendingContextClosure(authIndex, reason = "closure_cancelled") { @@ -1577,28 +1577,27 @@ class BrowserManager { return true; } - _scheduleContextClosureWhenIdle(authIndex, reason, activeQueueCount) { + _scheduleContextClosureWhenIdle(authIndex, reason) { if (!this.contexts.has(authIndex)) { return false; } const existingReason = this.pendingContextClosures.get(authIndex); if (existingReason) { this.logger.debug( - `[ContextPool] Context #${authIndex} is already pending close (${existingReason}), active queues: ${activeQueueCount}` + `[ContextPool] Context #${authIndex} is already pending close (${existingReason}), active queues are still present` ); return false; } this.pendingContextClosures.set(authIndex, reason); this.logger.info( - `[ContextPool] Deferring close for busy context #${authIndex} (${activeQueueCount} active queue(s), reason: ${reason})` + `[ContextPool] Deferring close for busy context #${authIndex} (active queue(s) present, reason: ${reason})` ); return false; } async _closeContextForPoolIfPossible(authIndex, reason) { - const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); - if (activeQueueCount > 0) { - return this._scheduleContextClosureWhenIdle(authIndex, reason, activeQueueCount); + if (this._hasActiveQueueForAuth(authIndex)) { + return this._scheduleContextClosureWhenIdle(authIndex, reason); } this.pendingContextClosures.delete(authIndex); @@ -1621,10 +1620,9 @@ class BrowserManager { return false; } - const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); - if (activeQueueCount > 0) { + if (this._hasActiveQueueForAuth(authIndex)) { this.logger.debug( - `[ContextPool] Pending close for context #${authIndex} is still waiting on ${activeQueueCount} active queue(s).` + `[ContextPool] Pending close for context #${authIndex} is still waiting on active queue(s).` ); return false; } @@ -1656,9 +1654,8 @@ class BrowserManager { const busy = []; for (const authIndex of indices) { - const activeQueueCount = this._getActiveQueueCountForAuth(authIndex); - if (activeQueueCount > 0) { - busy.push({ activeQueueCount, authIndex }); + if (this._hasActiveQueueForAuth(authIndex)) { + busy.push({ authIndex }); } else { idle.push(authIndex); } @@ -1895,7 +1892,7 @@ class BrowserManager { this.logger.info( `[ContextPool] Pre-cleanup before switch to #${targetAuthIndex}: immediate=[${toRemoveNow}], deferred=[${toDefer.map( - entry => `${entry.authIndex}:${entry.activeQueueCount}` + entry => entry.authIndex )}] (${this.contexts.size} ready + ${this.initializingContexts.size} initializing)` ); @@ -1904,7 +1901,7 @@ class BrowserManager { } for (const entry of toDefer) { - this._scheduleContextClosureWhenIdle(entry.authIndex, "pre_cleanup_for_switch", entry.activeQueueCount); + this._scheduleContextClosureWhenIdle(entry.authIndex, "pre_cleanup_for_switch"); } if (toDefer.length > 0) { @@ -1991,7 +1988,7 @@ class BrowserManager { this.logger.info( `[ContextPool] Rebalance: targets=[${[...targets]}], removeNow=[${idle}], removeDeferred=[${busy.map( - entry => `${entry.authIndex}:${entry.activeQueueCount}` + entry => entry.authIndex )}], candidates=[${candidates}]` ); @@ -2000,7 +1997,7 @@ class BrowserManager { } for (const entry of busy) { - this._scheduleContextClosureWhenIdle(entry.authIndex, "rebalance", entry.activeQueueCount); + this._scheduleContextClosureWhenIdle(entry.authIndex, "rebalance"); } // Preload candidates if ready and initializing contexts still leave room in the pool diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index e4327c2..fab465e 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -493,19 +493,13 @@ class ConnectionRegistry extends EventEmitter { return entry ? entry.requestAttemptId || null : null; } - /** - * Get the number of active message queues for a specific account - * @param {number} authIndex - The account index to inspect - * @returns {number} Number of active message queues - */ - getMessageQueueCountForAuth(authIndex) { - let count = 0; + _hasMessageQueueForAuth(authIndex) { for (const entry of this.messageQueues.values()) { if (entry.authIndex === authIndex) { - count++; + return true; } } - return count; + return false; } /** @@ -592,7 +586,7 @@ class ConnectionRegistry extends EventEmitter { if (!Number.isInteger(authIndex) || authIndex < 0) { return; } - if (this.getMessageQueueCountForAuth(authIndex) === 0) { + if (!this._hasMessageQueueForAuth(authIndex)) { this.emit("authQueuesDrained", authIndex); } } From 0dede783eb9c6980c17b4bb61ad9db55ddfa8b47 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 18:55:38 +0800 Subject: [PATCH 09/15] fix: update context closure logic to correctly indicate pending closures for busy contexts --- src/core/BrowserManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 3a532b2..4276d76 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -1586,13 +1586,13 @@ class BrowserManager { this.logger.debug( `[ContextPool] Context #${authIndex} is already pending close (${existingReason}), active queues are still present` ); - return false; + return true; } this.pendingContextClosures.set(authIndex, reason); this.logger.info( `[ContextPool] Deferring close for busy context #${authIndex} (active queue(s) present, reason: ${reason})` ); - return false; + return true; } async _closeContextForPoolIfPossible(authIndex, reason) { From 7fc9c70a757a4948f7b608228b471a9259d3ca33 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 19:15:55 +0800 Subject: [PATCH 10/15] fix: refactor message queue handling and improve logging for connection retrieval --- src/core/BrowserManager.js | 8 ++++---- src/core/ConnectionRegistry.js | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 4276d76..10dd199 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -1562,8 +1562,8 @@ class BrowserManager { } _hasActiveQueueForAuth(authIndex) { - if (this.connectionRegistry?._hasMessageQueueForAuth) { - return this.connectionRegistry._hasMessageQueueForAuth(authIndex); + if (this.connectionRegistry?.hasMessageQueueForAuth) { + return this.connectionRegistry.hasMessageQueueForAuth(authIndex); } return false; } @@ -1586,13 +1586,13 @@ class BrowserManager { this.logger.debug( `[ContextPool] Context #${authIndex} is already pending close (${existingReason}), active queues are still present` ); - return true; + return false; } this.pendingContextClosures.set(authIndex, reason); this.logger.info( `[ContextPool] Deferring close for busy context #${authIndex} (active queue(s) present, reason: ${reason})` ); - return true; + return false; } async _closeContextForPoolIfPossible(authIndex, reason) { diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index fab465e..2a4f1df 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -316,7 +316,7 @@ class ConnectionRegistry extends EventEmitter { if (connection) { this.logger.debug(`[Registry] Found WebSocket connection for authIndex=${authIndex}`); - } else { + } else if (this.logger.getLevel?.() === "DEBUG") { this.logger.debug( `[Registry] No WebSocket connection found for authIndex=${authIndex}. Available: [${Array.from(this.connectionsByAuth.keys()).join(", ")}]` ); @@ -493,7 +493,12 @@ class ConnectionRegistry extends EventEmitter { return entry ? entry.requestAttemptId || null : null; } - _hasMessageQueueForAuth(authIndex) { + /** + * Check whether a specific account has any active message queue. + * @param {number} authIndex - The account index to inspect + * @returns {boolean} True if at least one active message queue belongs to the account + */ + hasMessageQueueForAuth(authIndex) { for (const entry of this.messageQueues.values()) { if (entry.authIndex === authIndex) { return true; @@ -586,7 +591,7 @@ class ConnectionRegistry extends EventEmitter { if (!Number.isInteger(authIndex) || authIndex < 0) { return; } - if (!this._hasMessageQueueForAuth(authIndex)) { + if (!this.hasMessageQueueForAuth(authIndex)) { this.emit("authQueuesDrained", authIndex); } } From dd0bfbd317c51bbdbd433da098d513816de915e0 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 19:42:21 +0800 Subject: [PATCH 11/15] fix: correct indentation for emitting auth queues drained notification --- src/core/ConnectionRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ConnectionRegistry.js b/src/core/ConnectionRegistry.js index 2a4f1df..9618256 100644 --- a/src/core/ConnectionRegistry.js +++ b/src/core/ConnectionRegistry.js @@ -530,8 +530,8 @@ class ConnectionRegistry extends EventEmitter { this.logger.info( `[Registry] Force closed ${count} pending message queue(s) for account #${authIndex} (reason: ${reason})` ); + this._emitAuthQueuesDrainedIfNeeded(authIndex); } - this._emitAuthQueuesDrainedIfNeeded(authIndex); return count; } From ba274fe08d0ec4a75e240bd961f092635819256b Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 21:56:06 +0800 Subject: [PATCH 12/15] fix: add pre-cleanup step before switching browser context for recovery --- src/core/RequestHandler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index 153694f..e6c958e 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -732,6 +732,7 @@ class RequestHandler { this.authSwitcher.isSystemBusy = true; this.logger.info(`[System] Set isSystemBusy=true for direct recovery to account #${recoveryAuthIndex}`); + await this.browserManager.preCleanupForSwitch(recoveryAuthIndex); await this.browserManager.launchOrSwitchContext(recoveryAuthIndex); this.logger.info(`✅ [System] Browser successfully recovered to account #${recoveryAuthIndex}!`); From 9a35f21825372726086082a509256464821e7c03 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Tue, 12 May 2026 23:17:34 +0800 Subject: [PATCH 13/15] fix: add check for WebSocket connection readiness before retrying --- src/core/RequestHandler.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index e6c958e..ad75ce9 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -3417,6 +3417,17 @@ class RequestHandler { // Wait before the next retry await new Promise(resolve => setTimeout(resolve, this.retryDelay)); + if ( + !(await this._waitForSystemAndConnectionIfBusy(null, { + connectionMessage: "Service temporarily unavailable: Connection not ready before retry.", + })) + ) { + lastError = { + message: `WebSocket connection not ready before retry on account #${this.currentAuthIndex}.`, + status: 503, + }; + break; + } retryAttempt++; } } From 60c5217cef3ed1bdc483e8fa7aa19f6eafa5fd1b Mon Sep 17 00:00:00 2001 From: bbbugg Date: Wed, 13 May 2026 01:01:10 +0800 Subject: [PATCH 14/15] fix: remove unnecessary check for system busy state in connection wait logic --- src/core/RequestHandler.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index ad75ce9..7dcc8a5 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -537,10 +537,6 @@ class RequestHandler { } async _waitForSystemAndConnectionIfBusy(res = null, options = {}) { - if (!this.authSwitcher.isSystemBusy) { - return true; - } - const { busyMessage = "Server undergoing internal maintenance (account switching/recovery), please try again later.", connectionMessage = "Service temporarily unavailable: Connection not established after switching.", From cfa966e0cabf912f379ed9a707fbdf275c38d098 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Wed, 13 May 2026 01:39:51 +0800 Subject: [PATCH 15/15] fix: update system busy state handling in RequestHandler and StatusRoutes --- src/core/RequestHandler.js | 24 ++++++++++++++++++++++-- src/routes/StatusRoutes.js | 12 ++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index 7dcc8a5..e4a77fb 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -64,6 +64,10 @@ class RequestHandler { return this.authSwitcher.isSystemBusy; } + set isSystemBusy(value) { + this.authSwitcher.isSystemBusy = value === true; + } + _getUsageStatsService() { return this.serverSystem.usageStatsService || null; } @@ -3274,7 +3278,7 @@ class RequestHandler { const isClientDisconnect = reason === "client_disconnect"; const currentAuthIndex = this.currentAuthIndex; const isClosedAccountRetryable = reason === "context_closed" || reason === "page_closed"; - const canRetryOnCurrentAccount = + const canRetryOnCurrentAccountCandidate = !isClientDisconnect && isClosedAccountRetryable && retryAttempt < this.maxRetries && @@ -3282,7 +3286,23 @@ class RequestHandler { currentQueueAuthIndex >= 0 && Number.isInteger(currentAuthIndex) && currentAuthIndex >= 0 && - currentQueueAuthIndex !== currentAuthIndex && + currentQueueAuthIndex !== currentAuthIndex; + + if (canRetryOnCurrentAccountCandidate) { + const ready = await this._waitForSystemAndConnectionIfBusy(null, { + connectionMessage: "Service temporarily unavailable: Connection not ready before retry.", + }); + if (!ready) { + lastError = { + message: `WebSocket connection not ready before retry on account #${this.currentAuthIndex}.`, + status: 503, + }; + break; + } + } + + const canRetryOnCurrentAccount = + canRetryOnCurrentAccountCandidate && Boolean(this.connectionRegistry.getConnectionByAuth(currentAuthIndex, false)); if (isClientDisconnect) { diff --git a/src/routes/StatusRoutes.js b/src/routes/StatusRoutes.js index bfc2570..9b0b456 100644 --- a/src/routes/StatusRoutes.js +++ b/src/routes/StatusRoutes.js @@ -467,9 +467,9 @@ class StatusRoutes { `[WebUI] Current active account #${currentAuthIndex} was deleted. Closing context and connection...` ); // Set system busy flag to prevent new requests during cleanup - const previousBusy = this.serverSystem.isSystemBusy === true; + const previousBusy = this.serverSystem.requestHandler.isSystemBusy === true; if (!previousBusy) { - this.serverSystem.isSystemBusy = true; + this.serverSystem.requestHandler.isSystemBusy = true; } try { // 1. Terminate pending requests for the current account @@ -484,7 +484,7 @@ class StatusRoutes { } finally { // Reset system busy flag after cleanup completes if (!previousBusy) { - this.serverSystem.isSystemBusy = false; + this.serverSystem.requestHandler.isSystemBusy = false; } } } @@ -657,9 +657,9 @@ class StatusRoutes { if (targetIndex === currentAuthIndex) { // Set system busy flag to prevent new requests during cleanup - const previousBusy = this.serverSystem.isSystemBusy === true; + const previousBusy = this.serverSystem.requestHandler.isSystemBusy === true; if (!previousBusy) { - this.serverSystem.isSystemBusy = true; + this.serverSystem.requestHandler.isSystemBusy = true; } try { // If deleting the current account, terminate its pending requests first @@ -671,7 +671,7 @@ class StatusRoutes { } finally { // Reset system busy flag after cleanup completes if (!previousBusy) { - this.serverSystem.isSystemBusy = false; + this.serverSystem.requestHandler.isSystemBusy = false; } } } else {