mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 05:30:44 +08:00
fix: flaky WebSocket test, usage request queue, and test improvements
- Fix flaky WebSocket passthrough test: allow StatusNormalClosure after client close instead of requiring NoError (race condition fix) - Fix ratelimit 401 test: use PlatformOpenAI instead of PlatformGemini for OAuth token cache invalidation scenario (more accurate) - Add usageLoadQueue: Anthropic OAuth/setup-token accounts sharing the same proxy exit are serialized with 1-2s jitter to prevent upstream 429 - AccountUsageCell: add module-level usage cache (5min TTL), unmounted safety guard, and integrate enqueueUsageRequest for throttled fetching
This commit is contained in:
93
frontend/src/utils/usageLoadQueue.ts
Normal file
93
frontend/src/utils/usageLoadQueue.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Usage request scheduler — throttles Anthropic API calls by proxy exit.
|
||||
*
|
||||
* Anthropic OAuth/setup-token accounts sharing the same proxy exit are placed
|
||||
* into a serial queue with a random 1–2s delay between requests, preventing
|
||||
* upstream 429 rate-limit errors.
|
||||
*
|
||||
* Proxy identity = host:port:username — two proxy records pointing to the
|
||||
* same exit share a single queue. Accounts without a proxy go into a
|
||||
* "direct" queue.
|
||||
*
|
||||
* All other platforms bypass the queue and execute immediately.
|
||||
*/
|
||||
|
||||
import type { Account } from '@/types'
|
||||
|
||||
const GROUP_DELAY_MIN_MS = 1000
|
||||
const GROUP_DELAY_MAX_MS = 2000
|
||||
|
||||
type Task<T> = {
|
||||
fn: () => Promise<T>
|
||||
resolve: (value: T) => void
|
||||
reject: (reason: unknown) => void
|
||||
}
|
||||
|
||||
const queues = new Map<string, Task<unknown>[]>()
|
||||
const running = new Set<string>()
|
||||
|
||||
/** Whether this account needs throttled queuing. */
|
||||
function needsThrottle(account: Account): boolean {
|
||||
return (
|
||||
account.platform === 'anthropic' &&
|
||||
(account.type === 'oauth' || account.type === 'setup-token')
|
||||
)
|
||||
}
|
||||
|
||||
/** Build a queue key from proxy connection details. */
|
||||
function buildGroupKey(account: Account): string {
|
||||
const proxy = account.proxy
|
||||
const proxyIdentity = proxy
|
||||
? `${proxy.host}:${proxy.port}:${proxy.username || ''}`
|
||||
: 'direct'
|
||||
return `anthropic:${proxyIdentity}`
|
||||
}
|
||||
|
||||
async function drain(groupKey: string) {
|
||||
if (running.has(groupKey)) return
|
||||
running.add(groupKey)
|
||||
|
||||
const queue = queues.get(groupKey)
|
||||
while (queue && queue.length > 0) {
|
||||
const task = queue.shift()!
|
||||
try {
|
||||
const result = await task.fn()
|
||||
task.resolve(result)
|
||||
} catch (err) {
|
||||
task.reject(err)
|
||||
}
|
||||
if (queue.length > 0) {
|
||||
const jitter = GROUP_DELAY_MIN_MS + Math.random() * (GROUP_DELAY_MAX_MS - GROUP_DELAY_MIN_MS)
|
||||
await new Promise((r) => setTimeout(r, jitter))
|
||||
}
|
||||
}
|
||||
|
||||
running.delete(groupKey)
|
||||
queues.delete(groupKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a usage fetch. Anthropic accounts are queued by proxy exit;
|
||||
* all other platforms execute immediately.
|
||||
*/
|
||||
export function enqueueUsageRequest<T>(
|
||||
account: Account,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
// Non-Anthropic → fire immediately, no queuing
|
||||
if (!needsThrottle(account)) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
const key = buildGroupKey(account)
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let queue = queues.get(key)
|
||||
if (!queue) {
|
||||
queue = []
|
||||
queues.set(key, queue)
|
||||
}
|
||||
queue.push({ fn, resolve, reject } as Task<unknown>)
|
||||
drain(key)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user