forked from boos/BoosAPI
修复 weighted 路由失败退避时间溢出导致服务崩溃 (#354)
* fix: cap token router cooldown backoff * docs: explain token router cooldown cap
This commit is contained in:
@@ -691,4 +691,60 @@ describe('TokenRouter runtime cache', () => {
|
||||
expect(current?.cooldownLevel).toBe(0);
|
||||
expect(current?.cooldownUntil).toBeNull();
|
||||
});
|
||||
|
||||
it('caps weighted cooldowns before Date overflow for heavily failed channels', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'weighted-cooldown-cap-site',
|
||||
url: 'https://weighted-cooldown-cap.example.com',
|
||||
platform: 'new-api',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'weighted-cooldown-cap-user',
|
||||
accessToken: 'weighted-cooldown-cap-access-token',
|
||||
apiToken: 'weighted-cooldown-cap-api-token',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const token = await db.insert(schema.accountTokens).values({
|
||||
accountId: account.id,
|
||||
name: 'weighted-cooldown-cap-token',
|
||||
token: 'sk-weighted-cooldown-cap-token',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
}).returning().get();
|
||||
|
||||
const route = await db.insert(schema.tokenRoutes).values({
|
||||
modelPattern: 'gpt-5.4',
|
||||
routingStrategy: 'weighted',
|
||||
enabled: true,
|
||||
}).returning().get();
|
||||
|
||||
const channel = await db.insert(schema.routeChannels).values({
|
||||
routeId: route.id,
|
||||
accountId: account.id,
|
||||
tokenId: token.id,
|
||||
priority: 0,
|
||||
weight: 10,
|
||||
enabled: true,
|
||||
failCount: 57,
|
||||
}).returning().get();
|
||||
|
||||
// Weighted routes previously let Fibonacci backoff grow beyond the Date range.
|
||||
const router = new TokenRouter();
|
||||
|
||||
const startedAt = Date.now();
|
||||
await router.recordFailure(channel.id);
|
||||
const current = await db.select().from(schema.routeChannels)
|
||||
.where(eq(schema.routeChannels.id, channel.id))
|
||||
.get();
|
||||
const cooldownMs = Date.parse(String(current?.cooldownUntil || '')) - startedAt;
|
||||
|
||||
expect(current?.failCount).toBe(58);
|
||||
expect(Number.isFinite(Date.parse(String(current?.cooldownUntil || '')))).toBe(true);
|
||||
expect(cooldownMs).toBeGreaterThanOrEqual((30 * 24 * 60 * 60 - 5) * 1000);
|
||||
expect(cooldownMs).toBeLessThanOrEqual((30 * 24 * 60 * 60 + 5) * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,6 +72,8 @@ type SiteRuntimeHealthState = {
|
||||
|
||||
const FAILURE_BACKOFF_BASE_SEC = 15;
|
||||
const SHORT_WINDOW_LIMIT_COOLDOWN_MS = 5 * 60 * 1000;
|
||||
// Keep weighted-route backoff within the JavaScript Date range when fail counts grow large.
|
||||
const MAX_FAILURE_BACKOFF_SEC = 30 * 24 * 60 * 60;
|
||||
const MIN_EFFECTIVE_UNIT_COST = 1e-6;
|
||||
const ROUND_ROBIN_FAILURE_THRESHOLD = 3;
|
||||
const ROUND_ROBIN_COOLDOWN_LEVELS_SEC = [0, 10 * 60, 60 * 60, 24 * 60 * 60] as const;
|
||||
@@ -192,9 +194,13 @@ function fibonacciNumber(index: number): number {
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weighted-route failures use a Fibonacci backoff, but the resulting cooldown must stay
|
||||
* representable as a JavaScript Date for downstream `toISOString()` calls.
|
||||
*/
|
||||
function resolveFailureBackoffSec(failCount?: number | null): number {
|
||||
const normalizedFailCount = Math.max(1, Math.trunc(failCount ?? 0));
|
||||
return FAILURE_BACKOFF_BASE_SEC * fibonacciNumber(normalizedFailCount);
|
||||
return Math.min(FAILURE_BACKOFF_BASE_SEC * fibonacciNumber(normalizedFailCount), MAX_FAILURE_BACKOFF_SEC);
|
||||
}
|
||||
|
||||
function resolveRoundRobinCooldownSec(level: number): number {
|
||||
|
||||
Reference in New Issue
Block a user