fix(stats): added timeout to usage snapshot reads

Installed a five-second SQLite busy timeout before the read-only usage query and covered lock contention with a subprocess-backed regression test.

Fixes #7300
This commit is contained in:
roboomp
2026-08-01 18:50:37 +00:00
parent 09a7c86563
commit b1ce143a3c
3 changed files with 34 additions and 0 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed provider usage window stats silently showing no data during SQLite contention by installing a five-second busy timeout on read-only agent database connections ([#7300](https://github.com/can1357/oh-my-pi/issues/7300)).
## [17.1.2] - 2026-07-24
### Added
+1
View File
@@ -59,6 +59,7 @@ export function readUsageSnapshots(sinceMs: number, dbPath = getAgentDbPath()):
let db: Database | null = null;
try {
db = new Database(dbPath, { readonly: true });
db.run("PRAGMA busy_timeout = 5000");
const rows = db
.prepare(
`SELECT recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status
@@ -183,6 +183,35 @@ describe("readUsageSnapshots", () => {
usedFraction: 0.3,
});
});
it("waits for a contended database instead of returning no snapshots", async () => {
createAgentDb([snapshot({ recordedAt: T0, usedFraction: 0.3 })]);
const locker = Bun.spawn(
[
process.execPath,
"-e",
`import { Database } from "bun:sqlite";
const db = new Database(process.argv[1]);
db.run("BEGIN EXCLUSIVE");
process.stdout.write("locked\\n");
// This integration probe needs a real SQLite lock lifetime; fake timers cannot advance a separate process.
await Bun.sleep(100);
db.run("COMMIT");
db.close();`,
getAgentDbPath(),
],
{ env: { HOME: process.env.HOME ?? "", PATH: process.env.PATH ?? "" }, stdout: "pipe", stderr: "pipe" },
);
const output = locker.stdout.getReader();
const ready = await output.read();
output.releaseLock();
expect(new TextDecoder().decode(ready.value)).toContain("locked");
const rows = readUsageSnapshots(0);
const [exitCode, stderr] = await Promise.all([locker.exited, new Response(locker.stderr).text()]);
expect(exitCode, stderr).toBe(0);
expect(rows).toHaveLength(1);
});
});
describe("getProviderDashboardStats", () => {