mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
fix(db): install busy_timeout before the connection's first statement (#12394)
getDbInstance() ran PRAGMA journal_mode = WAL as the connection's first statement, before PRAGMA busy_timeout, and openSqliteDatabase() passes no driver-level timeout. A process opening the database while another closed its WAL connection — checkpoint plus WAL delete hold an EXCLUSIVE lock for a few hundred microseconds — therefore died with `database is locked` instead of waiting. That is the flake behind exclusive-connection-leases.test.ts on release/v3.8.51 runs 33525300898 and 33493797519 and on unrelated PR runs. The second half is worse than the flake: isTransientProbeError matched /SQLITE_BUSY/ against error.message, but both drivers report the plain text `database is locked` and put the code in .code / .errcode. A transient lock during the corruption probe therefore took the corrupt-database path and renamed the file to storage.sqlite.probe-failed-… with "Manual recovery required". The probe now recognises the drivers' real BUSY/PROTOCOL/IOERR signals. Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green. Thanks @pacocartones.
This commit is contained in:
@@ -1277,13 +1277,18 @@ export function getDbInstance(): SqliteDatabase {
|
||||
// selected on the server's primary DB path too, not only the backup-import
|
||||
// route.
|
||||
console.log(`[DB] Driver: ${db.driver} | file: ${sqliteFile}`);
|
||||
db.pragma("journal_mode = WAL");
|
||||
// better-sqlite3 is synchronous, so a contended write parks the Node event loop for up to
|
||||
// busy_timeout ms (a 0-CPU freeze that stacks under load → /health stops responding). The
|
||||
// hot-path writers here (usage_history, call_logs) are best-effort and the WinUI host opens
|
||||
// the same DB, so cap the block at 2s instead of 5s: normal writes complete in <1ms, and a
|
||||
// contended op can no longer freeze the loop past the host watchdog's 6s liveness probe.
|
||||
//
|
||||
// Install the busy handler before the connection's first statement. `journal_mode = WAL`
|
||||
// needs a SHARED lock, and another process closing its WAL connection briefly holds the
|
||||
// file EXCLUSIVE (checkpoint + WAL delete); node:sqlite opens with busy timeout 0, so with
|
||||
// the pragmas in the other order that window surfaced as `database is locked` at startup.
|
||||
db.pragma("busy_timeout = 2000");
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
db.pragma(`cache_size = -${DEFAULT_DATABASE_SETTINGS.optimization.cacheSize}`);
|
||||
db.pragma("temp_store = MEMORY");
|
||||
|
||||
@@ -22,7 +22,19 @@ import path from "node:path";
|
||||
*/
|
||||
export function isTransientProbeError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return /SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT/i.test(message);
|
||||
if (/SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database is locked/i.test(message)) {
|
||||
return true;
|
||||
}
|
||||
// The real drivers do not put the result-code name in the message: both
|
||||
// report plain "database is locked" for SQLITE_BUSY. better-sqlite3 carries
|
||||
// the name in `code`, node:sqlite the numeric primary code in `errcode`
|
||||
// (5 BUSY, 10 IOERR, 15 PROTOCOL; extended codes live in the high bits).
|
||||
// Without this, a transient lock during the probe was classified as
|
||||
// corruption and the database was renamed away.
|
||||
if (typeof error !== "object" || error === null) return false;
|
||||
const { code, errcode } = error as { code?: unknown; errcode?: unknown };
|
||||
if (typeof code === "string" && /^SQLITE_(BUSY|PROTOCOL|IOERR)/.test(code)) return true;
|
||||
return typeof errcode === "number" && [5, 10, 15].includes(errcode & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user