From 5170cbf1a7e77af93528f7b18b34116b1fb7831b Mon Sep 17 00:00:00 2001 From: Hrushikesh Yadav Date: Tue, 9 Jun 2026 10:40:36 +0530 Subject: [PATCH] fix: add 30s timeout to SQLite semaphore to prevent deadlock hangs The in-process Semaphore(1) guarding the single SQLite connection has no timeout, so any deadlock or re-entrant lock attempt causes the process to hang forever with no error. Add a 30s timeout to both the regular query acquirer and the transaction acquirer so contention surfaces as a clear SqlError instead of a silent futex hang. Closes #29395 --- packages/core/src/database/sqlite.bun.ts | 33 ++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts index e15f4c117e46..8db382866508 100644 --- a/packages/core/src/database/sqlite.bun.ts +++ b/packages/core/src/database/sqlite.bun.ts @@ -1,6 +1,7 @@ import { Database } from "bun:sqlite" import { drizzle } from "drizzle-orm/bun-sqlite" import * as Context from "effect/Context" +import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" @@ -119,12 +120,40 @@ const make = (options: Config) => }) const semaphore = yield* Semaphore.make(1) - const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const lockTimeout = Duration.seconds(30) + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)).pipe( + Effect.timeoutFail({ + duration: lockTimeout, + onTimeout: () => + new SqlError({ + reason: classifySqliteError(new Error("Timed out waiting for database lock after 30s"), { + message: "Database lock timeout", + operation: "query", + }), + }), + }), + ) const transactionAcquirer = Effect.uninterruptibleMask((restore) => { const fiber = Fiber.getCurrent()! const scope = Context.getUnsafe(fiber.context, Scope.Scope) return Effect.as( - Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + Effect.tap( + restore( + semaphore.take(1).pipe( + Effect.timeoutFail({ + duration: lockTimeout, + onTimeout: () => + new SqlError({ + reason: classifySqliteError( + new Error("Timed out waiting for database transaction lock after 30s — possible deadlock"), + { message: "Transaction lock timeout", operation: "transaction" }, + ), + }), + }), + ), + ), + () => Scope.addFinalizer(scope, semaphore.release(1)), + ), connection, ) })