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 { isAccountUnavailable, getUnavailableUntil } from "open-sse/services/accountFallback.js";
|
||||||
import * as log from "../utils/logger.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
|
* Get provider credentials from localDb
|
||||||
* Filters out unavailable accounts and returns the selected account based on strategy
|
* 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)
|
* @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account)
|
||||||
*/
|
*/
|
||||||
export async function getProviderCredentials(provider, excludeConnectionId = null) {
|
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 });
|
const connections = await getProviderConnections({ provider, isActive: true });
|
||||||
|
|
||||||
if (connections.length === 0) {
|
if (connections.length === 0) {
|
||||||
@@ -49,11 +60,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
|||||||
if (current && current.lastUsedAt && currentCount < stickyLimit) {
|
if (current && current.lastUsedAt && currentCount < stickyLimit) {
|
||||||
// Stay with current account
|
// Stay with current account
|
||||||
connection = current;
|
connection = current;
|
||||||
// Update lastUsedAt and increment count
|
// Update lastUsedAt and increment count (await to ensure persistence)
|
||||||
updateProviderConnection(connection.id, {
|
await updateProviderConnection(connection.id, {
|
||||||
lastUsedAt: new Date().toISOString(),
|
lastUsedAt: new Date().toISOString(),
|
||||||
consecutiveUseCount: (connection.consecutiveUseCount || 0) + 1
|
consecutiveUseCount: (connection.consecutiveUseCount || 0) + 1
|
||||||
}).catch(() => {});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Pick the least recently used (excluding current if possible)
|
// Pick the least recently used (excluding current if possible)
|
||||||
const sortedByOldest = [...availableConnections].sort((a, b) => {
|
const sortedByOldest = [...availableConnections].sort((a, b) => {
|
||||||
@@ -65,11 +76,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
|||||||
|
|
||||||
connection = sortedByOldest[0];
|
connection = sortedByOldest[0];
|
||||||
|
|
||||||
// Update lastUsedAt and reset count to 1
|
// Update lastUsedAt and reset count to 1 (await to ensure persistence)
|
||||||
updateProviderConnection(connection.id, {
|
await updateProviderConnection(connection.id, {
|
||||||
lastUsedAt: new Date().toISOString(),
|
lastUsedAt: new Date().toISOString(),
|
||||||
consecutiveUseCount: 1
|
consecutiveUseCount: 1
|
||||||
}).catch(() => {});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default: fill-first (already sorted by priority in getProviderConnections)
|
// Default: fill-first (already sorted by priority in getProviderConnections)
|
||||||
@@ -89,6 +100,9 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
|||||||
lastError: connection.lastError,
|
lastError: connection.lastError,
|
||||||
rateLimitedUntil: connection.rateLimitedUntil
|
rateLimitedUntil: connection.rateLimitedUntil
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
if (resolveMutex) resolveMutex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user