From dec653c7abcc66a1338156be22d4221d17294c52 Mon Sep 17 00:00:00 2001 From: Aaron Sachs <898627+asachs01@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:19:36 +0000 Subject: [PATCH] fix(dashboard): guard schema init against SQLITE_BUSY on concurrent build workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js's 'Collecting page data' step spins up multiple parallel workers that each import route modules to inspect their config — including db.ts, whose module-level `export const db = ... createDatabase()` runs on every import regardless of a route's `dynamic = 'force-dynamic'` (that only controls rendering, not module evaluation). On a fresh CI runner with no existing sqlite file, several workers can race to create the schema at once. busy_timeout=10000 was already set before initializeSchema() runs, and the WAL-mode switch right above it already has a catch-and-verify guard for this exact race — but the schema-init call itself had none. Caught in CI 2026-08-18 (run 32089273263): SqliteError during 'Collecting page data' while evaluating /api/agents/[name]. Applied the identical pattern already used for the WAL switch: on SQLITE_BUSY, check whether another worker's attempt already landed the schema (CREATE TABLE IF NOT EXISTS is idempotent, so 'someone else created it' is as good as 'we created it') and only re-throw if it didn't. npx tsc --noEmit clean; full dashboard suite (133 tests) passes. --- dashboard/src/lib/db.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dashboard/src/lib/db.ts b/dashboard/src/lib/db.ts index 3f55f2e43c..dfa52fbffd 100644 --- a/dashboard/src/lib/db.ts +++ b/dashboard/src/lib/db.ts @@ -40,8 +40,22 @@ function createDatabase(): Database.Database { db.pragma('synchronous = NORMAL'); db.pragma('foreign_keys = ON'); - // Run schema initialization - initializeSchema(db); + // Run schema initialization. Same SQLITE_BUSY race as the WAL switch above, + // on the same first-ever-access window (multiple Next.js build workers, no + // existing DB file yet) — busy_timeout already makes SQLite retry for 10s, + // but if every worker is doing the identical CREATE TABLE IF NOT EXISTS + // batch at once, that's not always enough headroom. Same guard: if we still + // hit SQLITE_BUSY after the timeout, check whether another worker's attempt + // already landed the schema (idempotent — IF NOT EXISTS — so "someone else + // created it" is as good as "we created it"); only re-throw if it didn't. + try { + initializeSchema(db); + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException & { code?: string }).code !== 'SQLITE_BUSY') throw err; + const rows = db.pragma("table_info('tasks')") as unknown[]; + if (rows.length === 0) throw err; + // Another worker already created the schema — we're fine. + } return db; }