Files
OmniRoute/src/lib/usage/callLogsBoundedQueries.ts
Xiangzhe 9e86a738ed perf(logging): bound call-log rotation work (#10125)
* perf(logging): bound call-log rotation work

* refactor(usage): extract call-log rotation/pruning from callLogs.ts to satisfy the file-size gate

Move the bounded rotation scheduler, orphan-artifact scanner, and row/overflow
pruning helpers (deleteCallLogsBefore, trimCallLogsToMaxRows,
cleanupOverflowCallLogFiles, cleanupOrphanCallLogFiles, rotateCallLogs,
scheduleCallLogRotation) into a new src/lib/usage/callLogRotation.ts module.
Pure extraction, no behavior change — callLogs.ts re-exports the same public
symbols so existing importers (usageDb.ts, compliance/index.ts, the
purge-logs route, and the rotation/cap test suite) are unaffected. Brings
callLogs.ts from 1108 to 787 lines, under the 1000-line file-size cap.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 07:52:46 -03:00

75 lines
2.8 KiB
TypeScript

import { getDbInstance } from "../db/core";
// #5618 — node:sqlite's StatementSync.all() materializes the ENTIRE result set as
// JS objects at once. On a large storage.sqlite (~170 MB+) an unbounded
// `SELECT … FROM call_logs` over the whole table blows the V8 heap during the
// startup cleanup pass (`cleanupExpiredLogs` → `rotateCallLogs`), crashing the
// daemon before it binds. These helpers page through call_logs in bounded chunks
// so peak memory stays flat regardless of table size.
const CALL_LOG_QUERY_PAGE = 5000;
/**
* Collect every non-null `artifact_relpath` referenced by call_logs, paging with
* LIMIT/OFFSET so a huge table never loads into memory in one `.all()`.
*/
export function collectReferencedArtifacts(): Set<string> {
const db = getDbInstance();
const referenced = new Set<string>();
const stmt = db.prepare(
"SELECT artifact_relpath FROM call_logs WHERE artifact_relpath IS NOT NULL LIMIT ? OFFSET ?"
);
for (let offset = 0; ; offset += CALL_LOG_QUERY_PAGE) {
const rows = stmt.all(CALL_LOG_QUERY_PAGE, offset) as Array<{
artifact_relpath: string | null;
}>;
for (const row of rows) {
if (typeof row.artifact_relpath === "string") referenced.add(row.artifact_relpath);
}
if (rows.length < CALL_LOG_QUERY_PAGE) break;
}
return referenced;
}
/**
* Select one bounded page of call_log ids older than `cutoff` (oldest first).
* Callers loop until it returns an empty page, deleting each batch, so the id
* list never grows to the full retention backlog at once.
*/
export function selectCallLogIdsBefore(cutoff: string, limit = CALL_LOG_QUERY_PAGE): string[] {
const db = getDbInstance();
const rows = db
.prepare("SELECT id FROM call_logs WHERE timestamp < ? ORDER BY timestamp ASC LIMIT ?")
.all(cutoff, limit) as Array<{ id: string }>;
return rows.map((row) => String(row.id));
}
export function selectOverflowArtifactPaths(maxEntries: number, limit: number): string[] {
const db = getDbInstance();
const rows = db
.prepare(
`SELECT artifact_relpath
FROM call_logs
WHERE artifact_relpath IS NOT NULL
ORDER BY timestamp DESC, id DESC
LIMIT ? OFFSET ?`
)
.all(limit, maxEntries) as Array<{ artifact_relpath: string }>;
return rows.map((row) => row.artifact_relpath);
}
export function findReferencedArtifacts(relativePaths: string[]): Set<string> {
if (relativePaths.length === 0) return new Set();
const db = getDbInstance();
const placeholders = relativePaths.map(() => "?").join(", ");
const rows = db
.prepare(
`SELECT DISTINCT artifact_relpath
FROM call_logs
WHERE artifact_relpath IN (${placeholders})
LIMIT ?`
)
.all(...relativePaths, relativePaths.length) as Array<{ artifact_relpath: string }>;
return new Set(rows.map((row) => row.artifact_relpath));
}