fix: prevent race conditions in sticky round-robin
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,6 +12,14 @@ 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) {
|
||||
// Acquire mutex to prevent race conditions
|
||||
const currentMutex = selectionMutex;
|
||||
let resolveMutex;
|
||||
selectionMutex = new Promise(resolve => { resolveMutex = resolve; });
|
||||
|
||||
try {
|
||||
await currentMutex;
|
||||
|
||||
const connections = await getProviderConnections({ provider, isActive: true });
|
||||
|
||||
if (connections.length === 0) {
|
||||
@@ -49,11 +60,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
if (current && current.lastUsedAt && currentCount < stickyLimit) {
|
||||
// Stay with current account
|
||||
connection = current;
|
||||
// Update lastUsedAt and increment count
|
||||
updateProviderConnection(connection.id, {
|
||||
// Update lastUsedAt and increment count (await to ensure persistence)
|
||||
await 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) => {
|
||||
@@ -65,11 +76,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
|
||||
connection = sortedByOldest[0];
|
||||
|
||||
// Update lastUsedAt and reset count to 1
|
||||
updateProviderConnection(connection.id, {
|
||||
// Update lastUsedAt and reset count to 1 (await to ensure persistence)
|
||||
await updateProviderConnection(connection.id, {
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
consecutiveUseCount: 1
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Default: fill-first (already sorted by priority in getProviderConnections)
|
||||
@@ -89,6 +100,9 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
lastError: connection.lastError,
|
||||
rateLimitedUntil: connection.rateLimitedUntil
|
||||
};
|
||||
} finally {
|
||||
if (resolveMutex) resolveMutex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user