mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 20:29:50 +00:00
fix: remove no-op autoFinalizeStatement wrapper and redundant busy_timeout (#2120)
better-sqlite3 11.x exposes no Statement.finalize() — the wrapper threw and swallowed a TypeError on every query (verified: 'Statement.finalize exists: undefined' in the runner image) while adding +122% per-statement overhead (3.90 -> 8.66 us/op, 200k-op in-container microbench) and freeing nothing. Statement lifecycle is GC-managed by the driver; drizzle-orm prepares fresh per query, so nothing accumulates unbounded. busy_timeout=5000 duplicates better-sqlite3's default timeout option, which already arms sqlite3_busy_timeout(db, 5000) at open (lib/database.js). With ENABLE_SQLITE_WAL_MODE unset the driver is now runtime-identical to pre-1.18.3 (zero pragmas). The env-gated WAL block stays: journal_mode is sticky in the DB file, so removing it would strand opted-in databases on WAL+synchronous=FULL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import type BetterSqlite3 from "better-sqlite3";
|
|
||||||
import * as schema from "./schema/schema";
|
import * as schema from "./schema/schema";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -12,64 +11,31 @@ export const exists = checkFileExists(location);
|
|||||||
|
|
||||||
bootstrapVolume();
|
bootstrapVolume();
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps better-sqlite3 Statement to call `finalize()` immediately after
|
|
||||||
* execution, freeing native sqlite3_stmt memory deterministically instead
|
|
||||||
* of waiting for GC. Fixes steady off-heap growth under load (#2120).
|
|
||||||
* WARNING: Finalizes after first execution — incompatible with drizzle's
|
|
||||||
* reusable .prepare() builders. No such usage exists in this codebase.
|
|
||||||
*/
|
|
||||||
function autoFinalizeStatement(
|
|
||||||
stmt: BetterSqlite3.Statement
|
|
||||||
): BetterSqlite3.Statement {
|
|
||||||
const wrapExec = <T extends (...args: any[]) => any>(fn: T): T => {
|
|
||||||
return function (this: any, ...args: any[]) {
|
|
||||||
try {
|
|
||||||
return fn.apply(this, args);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
// finalize() exists on the native Statement at runtime but
|
|
||||||
// is missing from @types/better-sqlite3.
|
|
||||||
(stmt as any).finalize();
|
|
||||||
} catch {
|
|
||||||
// Already finalized — harmless
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as unknown as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
stmt.run = wrapExec(stmt.run);
|
|
||||||
stmt.get = wrapExec(stmt.get);
|
|
||||||
stmt.all = wrapExec(stmt.all);
|
|
||||||
|
|
||||||
return stmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDb() {
|
function createDb() {
|
||||||
const sqlite = new Database(location);
|
const sqlite = new Database(location);
|
||||||
|
|
||||||
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
|
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
|
||||||
// Enable WAL mode — allows concurrent readers + single writer, preventing
|
// Enable WAL mode — allows concurrent readers + single writer, preventing
|
||||||
// contention across subsystems (verifySession, Traefik, audit, ping).
|
// contention across subsystems (verifySession, Traefik, audit, ping).
|
||||||
|
// NOTE: journal_mode persists in the DB file once set; unsetting this
|
||||||
|
// env var does NOT revert an existing WAL database.
|
||||||
sqlite.pragma("journal_mode = WAL");
|
sqlite.pragma("journal_mode = WAL");
|
||||||
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
|
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
|
||||||
sqlite.pragma("synchronous = NORMAL");
|
sqlite.pragma("synchronous = NORMAL");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
|
// No busy_timeout pragma: better-sqlite3 already arms
|
||||||
// retry loops that accumulate memory.
|
// sqlite3_busy_timeout(db, 5000) via its default `timeout` option
|
||||||
sqlite.pragma("busy_timeout = 5000");
|
// (lib/database.js), so an explicit pragma is redundant.
|
||||||
|
|
||||||
// Intentionally NOT setting cache_size or mmap_size: a large page cache plus
|
// Intentionally NOT setting cache_size or mmap_size: a large page cache plus
|
||||||
// a multi-hundred-MB mmap region inflate RSS and cause page-cache thrashing
|
// a multi-hundred-MB mmap region inflate RSS and cause page-cache thrashing
|
||||||
// on small (~1 GB) instances. Leave SQLite on its conservative defaults.
|
// on small (~1 GB) instances. Leave SQLite on its conservative defaults.
|
||||||
|
|
||||||
// Wrap prepare() so every drizzle-orm statement is auto-finalized after
|
// Intentionally NOT wrapping prepare()/statements: better-sqlite3 finalizes
|
||||||
// first use, preventing sqlite3_stmt accumulation between GC cycles.
|
// sqlite3_stmt in the Statement destructor at GC, and drizzle-orm prepares a
|
||||||
const originalPrepare = sqlite.prepare.bind(sqlite);
|
// fresh statement per query (no statement cache), so statements cannot
|
||||||
(sqlite as any).prepare = function autoFinalizePrepare(source: string) {
|
// accumulate. better-sqlite3 11.x exposes no Statement.finalize() at all.
|
||||||
return autoFinalizeStatement(originalPrepare(source));
|
|
||||||
};
|
|
||||||
|
|
||||||
return DrizzleSqlite(sqlite, {
|
return DrizzleSqlite(sqlite, {
|
||||||
schema
|
schema
|
||||||
|
|||||||
Reference in New Issue
Block a user