mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
fix(db): reduce hot-path persistence overhead (#2039)
Integrated into release/v3.8.0
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
loadBudget,
|
||||
loadCostEntries,
|
||||
loadCostEntriesInRange,
|
||||
loadCostTotal,
|
||||
saveBudget,
|
||||
saveBudgetResetLog,
|
||||
} from "../lib/db/domainState";
|
||||
@@ -238,8 +239,7 @@ function getActiveBudgetLimit(budget: NormalizedBudgetConfig): number {
|
||||
function getBudgetWindowTotal(apiKeyId: string, periodStartAt: number): number {
|
||||
try {
|
||||
return (
|
||||
sumEntries(toCostEntries(loadCostEntries(apiKeyId, periodStartAt))) +
|
||||
spendBatchWriter.getPendingCostTotal(apiKeyId, periodStartAt)
|
||||
loadCostTotal(apiKeyId, periodStartAt) + spendBatchWriter.getPendingCostTotal(apiKeyId, periodStartAt)
|
||||
);
|
||||
} catch {
|
||||
return 0;
|
||||
|
||||
@@ -381,13 +381,24 @@ export function batchSaveCostEntries(
|
||||
tx(entries);
|
||||
}
|
||||
|
||||
export function loadCostTotal(apiKeyId: string, sinceTimestamp: number) {
|
||||
ensureBudgetSchema();
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT COALESCE(SUM(cost), 0) AS total FROM domain_cost_history WHERE api_key_id = ? AND timestamp >= ?"
|
||||
)
|
||||
.get(apiKeyId, sinceTimestamp) as { total?: number } | undefined;
|
||||
return Number(row?.total || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cost entries for an API key within a time window.
|
||||
* @param {string} apiKeyId
|
||||
* @param {number} sinceTimestamp
|
||||
* @returns {Array<{cost: number, timestamp: number}>}
|
||||
*/
|
||||
export function loadCostEntries(apiKeyId, sinceTimestamp) {
|
||||
export function loadCostEntries(apiKeyId: string, sinceTimestamp: number) {
|
||||
ensureBudgetSchema();
|
||||
const db = getDbInstance();
|
||||
return db
|
||||
|
||||
10
src/lib/db/migrations/051_hot_path_db_indexes.sql
Normal file
10
src/lib/db/migrations/051_hot_path_db_indexes.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_dch_key_timestamp ON domain_cost_history(api_key_id, timestamp);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_history_api_key_id_timestamp
|
||||
ON usage_history(api_key_id, timestamp);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_history_api_key_name_timestamp
|
||||
ON usage_history(api_key_name, timestamp);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_call_logs_combo_name_timestamp
|
||||
ON call_logs(combo_name, timestamp);
|
||||
@@ -48,6 +48,16 @@ interface LegacyProxyConfig {
|
||||
keys?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
let proxyRegistryGeneration = 0;
|
||||
|
||||
function bumpProxyRegistryGeneration() {
|
||||
proxyRegistryGeneration++;
|
||||
}
|
||||
|
||||
export function getProxyRegistryGeneration() {
|
||||
return proxyRegistryGeneration;
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" ? (value as JsonRecord) : {};
|
||||
}
|
||||
@@ -189,6 +199,7 @@ export async function createProxy(payload: ProxyPayload) {
|
||||
);
|
||||
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyRegistryGeneration();
|
||||
return getProxyById(id, { includeSecrets: false });
|
||||
}
|
||||
|
||||
@@ -262,6 +273,7 @@ export async function updateProxy(id: string, payload: Partial<ProxyPayload>) {
|
||||
);
|
||||
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyRegistryGeneration();
|
||||
return getProxyById(id, { includeSecrets: false });
|
||||
}
|
||||
|
||||
@@ -332,6 +344,7 @@ export async function assignProxyToScope(
|
||||
normalizedScopeId
|
||||
);
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyRegistryGeneration();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -351,6 +364,7 @@ export async function assignProxyToScope(
|
||||
).run(proxyId, normalizedScope, normalizedScopeId, now, now);
|
||||
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyRegistryGeneration();
|
||||
|
||||
const row = db
|
||||
.prepare(
|
||||
@@ -383,6 +397,7 @@ export async function deleteProxyById(id: string, options?: { force?: boolean })
|
||||
|
||||
const result = db.prepare("DELETE FROM proxy_registry WHERE id = ?").run(id);
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyRegistryGeneration();
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getDbInstance } from "./core";
|
||||
import { backupDbFile } from "./backup";
|
||||
import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { invalidateDbCache } from "./readCache";
|
||||
import { resolveProxyForConnectionFromRegistry } from "./proxies";
|
||||
import { getProxyRegistryGeneration, resolveProxyForConnectionFromRegistry } from "./proxies";
|
||||
import { getComboModelProvider as getComboEntryProvider } from "@/lib/combos/steps";
|
||||
import { requestBodyLimitMbFromEnv } from "@/shared/constants/bodySize";
|
||||
|
||||
@@ -16,6 +16,37 @@ type PricingByProvider = Record<string, PricingModels>;
|
||||
export type PricingSource = "default" | "litellm" | "modelsDev" | "user";
|
||||
export type PricingSourceMap = Record<string, Record<string, PricingSource>>;
|
||||
type ProxyValue = JsonRecord | string | null;
|
||||
type ProxyResolutionResult = { proxy: ProxyValue; level: string; levelId: string | null; source?: string };
|
||||
type ProxyResolutionCacheEntry = {
|
||||
generation: number;
|
||||
registryGeneration: number;
|
||||
result: ProxyResolutionResult;
|
||||
};
|
||||
|
||||
const PROXY_RESOLUTION_CACHE_MAX_ENTRIES = 100;
|
||||
|
||||
let proxyConfigGeneration = 0;
|
||||
const proxyResolutionCache = new Map<string, ProxyResolutionCacheEntry>();
|
||||
|
||||
function bumpProxyConfigGeneration() {
|
||||
proxyConfigGeneration++;
|
||||
proxyResolutionCache.clear();
|
||||
}
|
||||
|
||||
function cacheProxyResolution(
|
||||
connectionId: string,
|
||||
generation: number,
|
||||
registryGeneration: number,
|
||||
result: ProxyResolutionResult
|
||||
) {
|
||||
if (generation !== proxyConfigGeneration) return;
|
||||
if (registryGeneration !== getProxyRegistryGeneration()) return;
|
||||
if (proxyResolutionCache.size >= PROXY_RESOLUTION_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = proxyResolutionCache.keys().next().value;
|
||||
if (oldestKey) proxyResolutionCache.delete(oldestKey);
|
||||
}
|
||||
proxyResolutionCache.set(connectionId, { generation, registryGeneration, result });
|
||||
}
|
||||
type ProxyMap = Record<string, ProxyValue>;
|
||||
|
||||
interface ProxyConfig {
|
||||
@@ -487,6 +518,7 @@ export async function setProxyForLevel(level: string, id: string | null, proxy:
|
||||
}
|
||||
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyConfigGeneration();
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -495,15 +527,31 @@ export async function deleteProxyForLevel(level: string, id: string | null) {
|
||||
}
|
||||
|
||||
export async function resolveProxyForConnection(connectionId: string) {
|
||||
const startGeneration = proxyConfigGeneration;
|
||||
const startRegistryGeneration = getProxyRegistryGeneration();
|
||||
const cached = proxyResolutionCache.get(connectionId);
|
||||
if (
|
||||
cached &&
|
||||
cached.generation === startGeneration &&
|
||||
cached.registryGeneration === startRegistryGeneration
|
||||
) {
|
||||
return cached.result;
|
||||
}
|
||||
|
||||
const registryResolved = await resolveProxyForConnectionFromRegistry(connectionId);
|
||||
if (registryResolved?.proxy) {
|
||||
if (registryResolved.level === "account") {
|
||||
cacheProxyResolution(connectionId, startGeneration, startRegistryGeneration, registryResolved);
|
||||
}
|
||||
return registryResolved;
|
||||
}
|
||||
|
||||
const config = await getProxyConfig();
|
||||
|
||||
if (connectionId && config.keys?.[connectionId]) {
|
||||
return { proxy: config.keys[connectionId], level: "key", levelId: connectionId };
|
||||
const result = { proxy: config.keys[connectionId], level: "key", levelId: connectionId };
|
||||
cacheProxyResolution(connectionId, startGeneration, startRegistryGeneration, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
const db = getDbInstance();
|
||||
@@ -551,7 +599,6 @@ export async function resolveProxyForConnection(connectionId: string) {
|
||||
if (config.global) {
|
||||
return { proxy: config.global, level: "global", levelId: null };
|
||||
}
|
||||
|
||||
return { proxy: null, level: "direct", levelId: null };
|
||||
}
|
||||
|
||||
@@ -588,6 +635,7 @@ export async function setProxyConfig(config: Record<string, unknown>) {
|
||||
tx();
|
||||
|
||||
backupDbFile("pre-write");
|
||||
bumpProxyConfigGeneration();
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ import {
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const CALL_LOG_ROTATE_THROTTLE_MS = 60_000;
|
||||
let lastCallLogRotationScheduledAt = 0;
|
||||
let callLogRotateInFlight = false;
|
||||
let callLogRotateScheduled = false;
|
||||
|
||||
type CallLogSummaryRow = {
|
||||
id: string;
|
||||
timestamp: string | null;
|
||||
@@ -721,16 +726,16 @@ export async function saveCallLog(entry: any) {
|
||||
requestSummary,
|
||||
});
|
||||
|
||||
rotateCallLogs();
|
||||
scheduleCallLogRotation();
|
||||
} catch (error) {
|
||||
console.error("[callLogs] Failed to save call log:", (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
export function rotateCallLogs() {
|
||||
if (!CALL_LOGS_DIR || !fs.existsSync(CALL_LOGS_DIR)) return;
|
||||
|
||||
try {
|
||||
if (!CALL_LOGS_DIR || !fs.existsSync(CALL_LOGS_DIR)) return;
|
||||
|
||||
const retentionMs = getCallLogRetentionDays() * 24 * 60 * 60 * 1000;
|
||||
const cutoff = new Date(Date.now() - retentionMs).toISOString();
|
||||
|
||||
@@ -743,12 +748,40 @@ export function rotateCallLogs() {
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPersistToDisk) {
|
||||
try {
|
||||
rotateCallLogs();
|
||||
} catch {
|
||||
// Best-effort startup cleanup.
|
||||
function runScheduledCallLogRotation() {
|
||||
if (callLogRotateInFlight) return;
|
||||
callLogRotateInFlight = true;
|
||||
setImmediate(() => {
|
||||
try {
|
||||
rotateCallLogs();
|
||||
} catch (error) {
|
||||
console.error("[callLogs] Failed to rotate request artifacts:", (error as Error).message);
|
||||
} finally {
|
||||
callLogRotateInFlight = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function scheduleCallLogRotation() {
|
||||
if (!CALL_LOGS_DIR) return;
|
||||
const elapsed = Date.now() - lastCallLogRotationScheduledAt;
|
||||
if (elapsed >= CALL_LOG_ROTATE_THROTTLE_MS) {
|
||||
lastCallLogRotationScheduledAt = Date.now();
|
||||
runScheduledCallLogRotation();
|
||||
return;
|
||||
}
|
||||
if (callLogRotateScheduled) return;
|
||||
callLogRotateScheduled = true;
|
||||
lastCallLogRotationScheduledAt = Date.now();
|
||||
const timer = setTimeout(() => {
|
||||
callLogRotateScheduled = false;
|
||||
runScheduledCallLogRotation();
|
||||
}, CALL_LOG_ROTATE_THROTTLE_MS - elapsed);
|
||||
timer.unref?.();
|
||||
}
|
||||
|
||||
if (shouldPersistToDisk && process.env.NODE_ENV !== "test") {
|
||||
scheduleCallLogRotation();
|
||||
}
|
||||
|
||||
export async function getCallLogs(filter: any = {}) {
|
||||
|
||||
@@ -205,6 +205,9 @@ test("rotateCallLogs removes expired rows and orphaned artifacts but keeps fresh
|
||||
.prepare("SELECT artifact_relpath FROM call_logs WHERE id = ?")
|
||||
.get("fresh-log");
|
||||
const freshAbsPath = path.join(TEST_DATA_DIR, "call_logs", (freshRow as any).artifact_relpath);
|
||||
|
||||
callLogs.rotateCallLogs();
|
||||
|
||||
assert.equal(
|
||||
(
|
||||
core
|
||||
@@ -217,8 +220,6 @@ test("rotateCallLogs removes expired rows and orphaned artifacts but keeps fresh
|
||||
assert.equal(fs.existsSync(oldAbsPath), false);
|
||||
assert.equal(fs.existsSync(freshAbsPath), true);
|
||||
|
||||
callLogs.rotateCallLogs();
|
||||
|
||||
const db = core.getDbInstance();
|
||||
assert.equal(
|
||||
(db.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?").get("fresh-log") as any).cnt,
|
||||
|
||||
Reference in New Issue
Block a user