mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(providers): stop marking a multi-quota-window provider exhausted when only some windows are depleted (LIMIT-200 snapshot eviction drops idle healthy windows) (#8431) (#8445)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
This commit is contained in:
committed by
GitHub
parent
36f8fd1005
commit
9a78ea2225
1
changelog.d/fixes/8431-multiwindow-quota-eviction.md
Normal file
1
changelog.d/fixes/8431-multiwindow-quota-eviction.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): stop marking a multi-quota-window provider exhausted when only some windows are depleted (LIMIT-200 snapshot eviction drops idle healthy windows) (#8431)
|
||||
@@ -84,29 +84,37 @@ export function getQuotaSnapshots(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single latest snapshot row for each distinct `window_key`
|
||||
* ever observed for this connection.
|
||||
*
|
||||
* Deliberately NOT a "most recent N rows across all windows" query: a
|
||||
* connection with many quota windows where only a subset actively churn
|
||||
* (frequent writes as they drain) and the rest stay idle/healthy (a single
|
||||
* old row each, thanks to the #4438 no-op-write dedup) would otherwise have
|
||||
* its recent-rows slice flooded entirely by the hot windows, silently
|
||||
* evicting the idle windows from rehydration (#8431). Scoping "latest" PER
|
||||
* window_key via a window function keeps every window visible regardless of
|
||||
* how skewed the write frequency is across windows.
|
||||
*/
|
||||
export function getLatestQuotaSnapshotsForConnection(connectionId: string): QuotaSnapshotRow[] {
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT * FROM quota_snapshots
|
||||
WHERE connection_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 200`
|
||||
`SELECT * FROM (
|
||||
SELECT *, ROW_NUMBER() OVER (
|
||||
PARTITION BY window_key ORDER BY created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM quota_snapshots
|
||||
WHERE connection_id = ?
|
||||
)
|
||||
WHERE rn = 1`
|
||||
)
|
||||
.all(connectionId);
|
||||
const latestByWindow = new Map<string, QuotaSnapshotRow>();
|
||||
|
||||
for (const row of rows) {
|
||||
const snapshot = rowToCamel(row) as unknown as QuotaSnapshotRow;
|
||||
const windowKey =
|
||||
(snapshot as unknown as { windowKey?: string }).windowKey ?? snapshot.window_key;
|
||||
if (!windowKey || latestByWindow.has(windowKey)) continue;
|
||||
latestByWindow.set(windowKey, snapshot);
|
||||
}
|
||||
|
||||
return [...latestByWindow.values()];
|
||||
return rows.map((row) => rowToCamel(row) as unknown as QuotaSnapshotRow);
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("no such table")) {
|
||||
return [];
|
||||
|
||||
119
tests/unit/8431-multiwindow-quota-eviction.test.ts
Normal file
119
tests/unit/8431-multiwindow-quota-eviction.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* #8431 — a provider with many quota windows (e.g. codebuddy-cn: 1 Monthly +
|
||||
* up to 8 Bonus Packs) can be wrongly reported as fully exhausted after a
|
||||
* fresh boot (empty in-memory cache) even though most windows still have
|
||||
* balance.
|
||||
*
|
||||
* Root cause: `getLatestQuotaSnapshotsForConnection()` fetched the most
|
||||
* recent 200 rows for the connection (across ALL windows), then deduped by
|
||||
* `window_key` *inside* that slice. A connection where a few windows churn
|
||||
* frequently (draining, so they keep writing fresh rows) and the rest stay
|
||||
* idle/healthy (a single old row each) can have its top-200 slice entirely
|
||||
* flooded by the hot windows once they collectively accumulate >200 rows.
|
||||
* The idle-but-healthy windows' only row falls outside the slice and is
|
||||
* silently dropped from rehydration, so `isExhausted()` — which is correct
|
||||
* on the data it's given — reports the connection exhausted because every
|
||||
* window it was handed genuinely is at 0%.
|
||||
*
|
||||
* Regression guard: without the fix, only the 3 hot windows survive
|
||||
* rehydration (of 9 total) and `isQuotaExhaustedForRequest` wrongly reports
|
||||
* `true`. With the fix, all 9 windows survive and the request stays
|
||||
* eligible because 6 of the 9 windows still have balance.
|
||||
*/
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-quota-8431-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const coreDb = await import("../../src/lib/db/core.ts");
|
||||
const quotaSnapshotsDb = await import("../../src/lib/db/quotaSnapshots.ts");
|
||||
const quotaCache = await import("../../src/domain/quotaCache.ts");
|
||||
|
||||
test.after(() => {
|
||||
coreDb.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const COLD_WINDOWS = ["Bonus Pack 1", "Bonus Pack 2", "Bonus Pack 3", "Bonus Pack 4", "Weekly", "Daily"];
|
||||
const HOT_WINDOWS = ["Monthly", "Bonus Pack 5", "Bonus Pack 6"];
|
||||
|
||||
test("#8431 idle healthy windows survive rehydration even when hot windows accumulate >200 rows", () => {
|
||||
const connectionId = "conn-codebuddy-cn-8431";
|
||||
const provider = "codebuddy-cn";
|
||||
|
||||
// 6 cold, healthy windows — each written exactly once (mirrors the #4438
|
||||
// no-op-write dedup for windows whose value never changes) and BEFORE the
|
||||
// hot rows below.
|
||||
for (const windowKey of COLD_WINDOWS) {
|
||||
quotaSnapshotsDb.saveQuotaSnapshot({
|
||||
provider,
|
||||
connection_id: connectionId,
|
||||
window_key: windowKey,
|
||||
remaining_percentage: 80,
|
||||
is_exhausted: 0,
|
||||
next_reset_at: "2099-01-01T00:00:00.000Z",
|
||||
window_duration_ms: null,
|
||||
raw_data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 3 hot, actively-draining windows — 70 iterations x 3 windows = 210 rows,
|
||||
// all created after the cold rows, exceeding the old LIMIT 200.
|
||||
for (let i = 0; i < 70; i++) {
|
||||
for (const windowKey of HOT_WINDOWS) {
|
||||
quotaSnapshotsDb.saveQuotaSnapshot({
|
||||
provider,
|
||||
connection_id: connectionId,
|
||||
window_key: windowKey,
|
||||
remaining_percentage: 0,
|
||||
is_exhausted: 1,
|
||||
next_reset_at: "2099-01-08T00:00:00.000Z",
|
||||
window_duration_ms: null,
|
||||
raw_data: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rehydrated = quotaSnapshotsDb.getLatestQuotaSnapshotsForConnection(connectionId);
|
||||
const rehydratedKeys = rehydrated
|
||||
.map((s) => (s as unknown as { windowKey?: string }).windowKey ?? s.window_key)
|
||||
.sort();
|
||||
|
||||
assert.equal(
|
||||
rehydrated.length,
|
||||
9,
|
||||
`expected all 9 windows to survive rehydration, got ${rehydrated.length}: ${rehydratedKeys.join(", ")}`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
quotaCache.isQuotaExhaustedForRequest(connectionId, provider, "deepseek-v4-pro"),
|
||||
false,
|
||||
"6 of 9 windows still have balance — the connection must not be reported as exhausted"
|
||||
);
|
||||
});
|
||||
|
||||
test("#8431 a single-window provider is still correctly reported exhausted", () => {
|
||||
const connectionId = "conn-single-window-8431";
|
||||
const provider = "openai";
|
||||
|
||||
quotaSnapshotsDb.saveQuotaSnapshot({
|
||||
provider,
|
||||
connection_id: connectionId,
|
||||
window_key: "weekly",
|
||||
remaining_percentage: 0,
|
||||
is_exhausted: 1,
|
||||
next_reset_at: null,
|
||||
window_duration_ms: null,
|
||||
raw_data: null,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
quotaCache.isQuotaExhaustedForRequest(connectionId, provider, "gpt-5"),
|
||||
true,
|
||||
"single depleted window must still correctly report exhaustion"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user