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?.cooldownLevel).toBe(0);
|
||||||
expect(current?.cooldownUntil).toBeNull();
|
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 FAILURE_BACKOFF_BASE_SEC = 15;
|
||||||
const SHORT_WINDOW_LIMIT_COOLDOWN_MS = 5 * 60 * 1000;
|
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 MIN_EFFECTIVE_UNIT_COST = 1e-6;
|
||||||
const ROUND_ROBIN_FAILURE_THRESHOLD = 3;
|
const ROUND_ROBIN_FAILURE_THRESHOLD = 3;
|
||||||
const ROUND_ROBIN_COOLDOWN_LEVELS_SEC = [0, 10 * 60, 60 * 60, 24 * 60 * 60] as const;
|
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;
|
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 {
|
function resolveFailureBackoffSec(failCount?: number | null): number {
|
||||||
const normalizedFailCount = Math.max(1, Math.trunc(failCount ?? 0));
|
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 {
|
function resolveRoundRobinCooldownSec(level: number): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user