From d038ba2195e08b152c11d28aee18ba5e10deeb07 Mon Sep 17 00:00:00 2001 From: Gauransh Mathur Date: Fri, 16 Oct 2026 14:31:18 +0000 Subject: [PATCH 1/2] test(beyondleetcode): C43b multi-user fixture waits for the database lock and retries a busy transaction Co-Authored-By: Claude Sonnet 5.5 --- src/routes/users/multi-fixture.ts | 83 +++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/src/routes/users/multi-fixture.ts b/src/routes/users/multi-fixture.ts index b273b28..52bba78 100644 --- a/src/routes/users/multi-fixture.ts +++ b/src/routes/users/multi-fixture.ts @@ -1,5 +1,7 @@ import { readdirSync, readFileSync } from 'node:fs'; -import { createDb } from '../../lib/server/db.ts'; +import { PrismaLibSql } from '@prisma/adapter-libsql'; +import { PrismaClient } from '../../../prisma/generated/client'; +import type { Db } from '../../lib/server/db.ts'; import { createAuth } from '../../lib/server/identity/auth.ts'; // Test-only, and it adds no production code path. Browser tests of the Multi-user server (4174) that need @@ -42,6 +44,51 @@ function serverEnvironment(): ServerTarget { /** A Multi-user server's database file, signing secret and public origin: what a second Better Auth needs to act on it. */ export type ServerTarget = { database: string; secret: string; origin: string }; +/** + * Why the fixture opens its own client instead of `createDb`: libSQL's busy timeout defaults to 0, so a write + * that meets another process's write lock fails at once with SQLITE_BUSY, which Prisma reports as P1008 + * ("Operation has timed out"). Four workers write to the one shared file (and the server writes to it too), + * so this happened now and then. The client here waits up to BUSY_TIMEOUT_MS for a lock. That alone is not + * enough: Prisma's libSQL adapter opens transactions as deferred (read first, then write), and SQLite refuses + * a read-to-write upgrade at once, whatever the timeout, when another connection already holds the write + * lock. Only running the whole transaction again helps, which is `retryWhenBusy` below. The app's own client + * is not touched (production code is out of this card). + */ +const BUSY_TIMEOUT_MS = 10_000; + +/** Prisma's "Operation has timed out" (SQLITE_BUSY), the only error the retry below absorbs. */ +function isBusy(error: unknown): boolean { + let current: unknown = error; + for (let depth = 0; current && depth < 5; depth++) { + const { code, cause } = current as { code?: unknown; cause?: unknown }; + if (code === 'P1008') return true; + current = cause; + } + return false; +} + +/** + * Runs `operation` again (at most `attempts` times in all) when the database was busy during it, and only + * then; any other error is thrown at once. Better Auth catches a failed insert and throws its own "Failed to + * create user", so the busy error is not on what comes out. `busyCount` says whether one happened inside. + */ +export async function retryWhenBusy( + busyCount: () => number, + operation: () => Promise, + attempts = 4 +): Promise { + for (let attempt = 1; ; attempt++) { + const before = busyCount(); + try { + return await operation(); + } catch (error) { + const busy = isBusy(error) || busyCount() > before; + if (attempt >= attempts || !busy) throw error; + await new Promise((resolve) => setTimeout(resolve, 100 * attempt + Math.random() * 100)); + } + } +} + export const LEARNER_PASSWORD = 'correct horse battery staple'; /** The `Set-Cookie` lines of a sign-in as Playwright `storageState`: this browser context is that user. */ @@ -71,24 +118,40 @@ export type Session = ReturnType; /** The shared 4174 server's database, or another Multi-user server's when `target` names it. */ export async function openMultiUserDatabase(target: ServerTarget = serverEnvironment()) { const { database, secret, origin } = target; - const db = createDb(database); + let busyHits = 0; + const db = new PrismaClient({ + adapter: new PrismaLibSql({ url: database, timeout: BUSY_TIMEOUT_MS }) + }).$extends({ + query: { + async $allOperations({ args, query }) { + try { + return await query(args); + } catch (error) { + if (isBusy(error)) busyHits++; + throw error; + } + } + } + }); + const busyCount = () => busyHits; // Admits every sign-up: this instance is only ever driven from the test process, never over HTTP. - const auth = createAuth({ db, origin, secret, admitSignUp: () => ({}) }); + const auth = createAuth({ db: db as unknown as Db, origin, secret, admitSignUp: () => ({}) }); return { db, /** A new Account (a learner), or the one with this email from an earlier run of the same test. */ async ensureLearner({ name, email }: { name: string; email: string }) { - if (!(await db.user.findUnique({ where: { email } }))) { - await auth.api.signUpEmail({ body: { name, email, password: LEARNER_PASSWORD } }); - } + await retryWhenBusy(busyCount, async () => { + if (!(await db.user.findUnique({ where: { email } }))) { + await auth.api.signUpEmail({ body: { name, email, password: LEARNER_PASSWORD } }); + } + }); }, /** A fresh session for an Account, as the cookie a browser would hold after signing in. */ async signIn(email: string, password: string): Promise { - const { headers } = await auth.api.signInEmail({ - body: { email, password }, - returnHeaders: true - }); + const { headers } = await retryWhenBusy(busyCount, () => + auth.api.signInEmail({ body: { email, password }, returnHeaders: true }) + ); return sessionOf(headers.getSetCookie()); }, close: () => db.$disconnect() From 4a4a0585c689facc587952f5be4b8628bf5f2d90 Mon Sep 17 00:00:00 2001 From: Gauransh Mathur Date: Fri, 16 Oct 2026 14:51:39 +0000 Subject: [PATCH 2/2] test(beyondleetcode): C43b write-ahead logging on the shared Multi-user test database Co-Authored-By: Claude Sonnet 5.5 --- playwright.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/playwright.config.ts b/playwright.config.ts index 8400801..4cf474f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -32,6 +32,11 @@ const multi = [ 'export BETTER_AUTH_SECRET=e2e-only-secret-e2e-only-secret-e2e-only-0123', ...common, 'bunx prisma migrate deploy', + // Test-only: write-ahead logging, so a reader never blocks a writer. The Multi-user files share this one + // file between the server and four test processes; the app's own client waits for no lock (busy timeout 0) + // and opens deferred transactions, which in the default journal fail at once when anyone else is writing. + // The mode is stored in the file, so the server finds it on. + `bun -e 'new (require("bun:sqlite").Database)(process.env.DATABASE_URL.slice(5)).exec("PRAGMA journal_mode=WAL")'`, 'PORT=4174 bun ./build/index.js' ].join(' && ');