From 3ad2f8dc580dc264537c96c6a277701d0521d16e Mon Sep 17 00:00:00 2001 From: Catalin Stanciu Date: Tue, 6 Jan 2026 22:43:15 +0200 Subject: [PATCH] fix: prevent race conditions in sticky round-robin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a mutex to serialize account selection and updates in the proxy engine. This ensures that concurrent requests respect the sticky limit and don't distribute to the same account simultaneously. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/sse/services/auth.js | 148 +++++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 67 deletions(-) diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js index 45fa907c..16b87b92 100644 --- a/src/sse/services/auth.js +++ b/src/sse/services/auth.js @@ -2,6 +2,9 @@ import { getProviderConnections, validateApiKey, updateProviderConnection, getSe import { isAccountUnavailable, getUnavailableUntil } from "open-sse/services/accountFallback.js"; import * as log from "../utils/logger.js"; +// Mutex to prevent race conditions during account selection +let selectionMutex = Promise.resolve(); + /** * Get provider credentials from localDb * Filters out unavailable accounts and returns the selected account based on strategy @@ -9,86 +12,97 @@ import * as log from "../utils/logger.js"; * @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account) */ export async function getProviderCredentials(provider, excludeConnectionId = null) { - const connections = await getProviderConnections({ provider, isActive: true }); + // Acquire mutex to prevent race conditions + const currentMutex = selectionMutex; + let resolveMutex; + selectionMutex = new Promise(resolve => { resolveMutex = resolve; }); - if (connections.length === 0) { - log.warn("AUTH", `No credentials for ${provider}`); - return null; - } + try { + await currentMutex; - // Filter out unavailable accounts and excluded connection - const availableConnections = connections.filter(c => { - if (excludeConnectionId && c.id === excludeConnectionId) return false; - if (isAccountUnavailable(c.rateLimitedUntil)) return false; - return true; - }); + const connections = await getProviderConnections({ provider, isActive: true }); - if (availableConnections.length === 0) { - log.warn("AUTH", `All ${connections.length} accounts for ${provider} unavailable`); - return null; - } + if (connections.length === 0) { + log.warn("AUTH", `No credentials for ${provider}`); + return null; + } - const settings = await getSettings(); - const strategy = settings.fallbackStrategy || "fill-first"; - - let connection; - if (strategy === "round-robin") { - const stickyLimit = settings.stickyRoundRobinLimit || 3; - - // Sort by lastUsed (most recent first) to find current candidate - const byRecency = [...availableConnections].sort((a, b) => { - if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); - if (!a.lastUsedAt) return 1; - if (!b.lastUsedAt) return -1; - return new Date(b.lastUsedAt) - new Date(a.lastUsedAt); + // Filter out unavailable accounts and excluded connection + const availableConnections = connections.filter(c => { + if (excludeConnectionId && c.id === excludeConnectionId) return false; + if (isAccountUnavailable(c.rateLimitedUntil)) return false; + return true; }); - const current = byRecency[0]; - const currentCount = current?.consecutiveUseCount || 0; + if (availableConnections.length === 0) { + log.warn("AUTH", `All ${connections.length} accounts for ${provider} unavailable`); + return null; + } - if (current && current.lastUsedAt && currentCount < stickyLimit) { - // Stay with current account - connection = current; - // Update lastUsedAt and increment count - updateProviderConnection(connection.id, { - lastUsedAt: new Date().toISOString(), - consecutiveUseCount: (connection.consecutiveUseCount || 0) + 1 - }).catch(() => {}); - } else { - // Pick the least recently used (excluding current if possible) - const sortedByOldest = [...availableConnections].sort((a, b) => { + const settings = await getSettings(); + const strategy = settings.fallbackStrategy || "fill-first"; + + let connection; + if (strategy === "round-robin") { + const stickyLimit = settings.stickyRoundRobinLimit || 3; + + // Sort by lastUsed (most recent first) to find current candidate + const byRecency = [...availableConnections].sort((a, b) => { if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); - if (!a.lastUsedAt) return -1; - if (!b.lastUsedAt) return 1; - return new Date(a.lastUsedAt) - new Date(b.lastUsedAt); + if (!a.lastUsedAt) return 1; + if (!b.lastUsedAt) return -1; + return new Date(b.lastUsedAt) - new Date(a.lastUsedAt); }); - connection = sortedByOldest[0]; + const current = byRecency[0]; + const currentCount = current?.consecutiveUseCount || 0; - // Update lastUsedAt and reset count to 1 - updateProviderConnection(connection.id, { - lastUsedAt: new Date().toISOString(), - consecutiveUseCount: 1 - }).catch(() => {}); + if (current && current.lastUsedAt && currentCount < stickyLimit) { + // Stay with current account + connection = current; + // Update lastUsedAt and increment count (await to ensure persistence) + await updateProviderConnection(connection.id, { + lastUsedAt: new Date().toISOString(), + consecutiveUseCount: (connection.consecutiveUseCount || 0) + 1 + }); + } else { + // Pick the least recently used (excluding current if possible) + const sortedByOldest = [...availableConnections].sort((a, b) => { + if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); + if (!a.lastUsedAt) return -1; + if (!b.lastUsedAt) return 1; + return new Date(a.lastUsedAt) - new Date(b.lastUsedAt); + }); + + connection = sortedByOldest[0]; + + // Update lastUsedAt and reset count to 1 (await to ensure persistence) + await updateProviderConnection(connection.id, { + lastUsedAt: new Date().toISOString(), + consecutiveUseCount: 1 + }); + } + } else { + // Default: fill-first (already sorted by priority in getProviderConnections) + connection = availableConnections[0]; } - } else { - // Default: fill-first (already sorted by priority in getProviderConnections) - connection = availableConnections[0]; - } - return { - apiKey: connection.apiKey, - accessToken: connection.accessToken, - refreshToken: connection.refreshToken, - projectId: connection.projectId, - copilotToken: connection.providerSpecificData?.copilotToken, - providerSpecificData: connection.providerSpecificData, - connectionId: connection.id, - // Include current status for optimization check - testStatus: connection.testStatus, - lastError: connection.lastError, - rateLimitedUntil: connection.rateLimitedUntil - }; + return { + apiKey: connection.apiKey, + accessToken: connection.accessToken, + refreshToken: connection.refreshToken, + projectId: connection.projectId, + copilotToken: connection.providerSpecificData?.copilotToken, + providerSpecificData: connection.providerSpecificData, + connectionId: connection.id, + // Include current status for optimization check + testStatus: connection.testStatus, + lastError: connection.lastError, + rateLimitedUntil: connection.rateLimitedUntil + }; + } finally { + if (resolveMutex) resolveMutex(); + } } /**