修复 weighted 路由失败退避时间溢出导致服务崩溃 (#354)

* fix: cap token router cooldown backoff

* docs: explain token router cooldown cap
This commit is contained in:
nodca
2026-04-01 12:29:05 +08:00
committed by GitHub
parent 4096c757d4
commit 6a55effd3b
2 changed files with 63 additions and 1 deletions
@@ -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);
});
});
+7 -1
View File
@@ -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 {