Skip to content

Add timeout config option to set busy timeout on local sqlite3 connections#345

Open
roaminro wants to merge 1 commit into
tursodatabase:mainfrom
roaminro:feat/timeout-config-passthrough
Open

Add timeout config option to set busy timeout on local sqlite3 connections#345
roaminro wants to merge 1 commit into
tursodatabase:mainfrom
roaminro:feat/timeout-config-passthrough

Conversation

@roaminro

Copy link
Copy Markdown

Summary

Adds a timeout option to createClient() config that sets the SQLite busy timeout (in milliseconds) on local file: databases:

const client = createClient({ url: "file:local.db", timeout: 5000 });

Fixes #288.

Problem

There is currently no reliable way to set a busy timeout on a local database:

  • The native libsql binding already supports a busy timeout (opts?.timeout ?? 0.0databaseOpen(...)), but the client never passes it through, so every connection opens with no busy handler.
  • Applying PRAGMA busy_timeout manually doesn't stick: transaction() hands the client's current connection to the transaction and lazily creates a new connection for the next statement (see Interleaved transactions no longer work #104 / sqlite3: use one connection per transaction #105), silently dropping the pragma. After the first interactive transaction, any lock contention — e.g. two processes sharing one database file — fails instantly with SQLITE_BUSY: database is locked instead of waiting.

This bites real applications: several downstream projects (we hit this in mastra, Cherry Studio hit it too) ended up patching around it by replaying pragmas after every transaction() call.

Solution

Pass timeout through expandConfig into the Database options in the sqlite3 client. Since those options are reused for every lazily-created connection (#getDb()), the busy timeout applies natively to all connections the client opens, including the ones created after transaction() — no pragma replay needed.

Remote clients ignore the option; it only affects local files (same scope as the native binding's support).

Testing

  • expandConfig unit test for the new option
  • PRAGMA busy_timeout reflects the configured value on a fresh client
  • the timeout survives the connection swap caused by transaction() (regression test for busy_timeout is seemingly ignored (interleaving transactions will always fail) #288)
  • behavioral test: with a second connection holding a write lock, a client without timeout fails immediately with SQLITE_BUSY, while a client with timeout: 500 waits for the busy handler before giving up
URL=file:///tmp/libsql-client-test.db npx jest --runInBand client.test
Tests: 11 skipped, 141 passed, 152 total

npm run typecheck and npm run format:check pass on the changed packages.

The native libsql binding already supports a busy timeout via its
Database options ('opts?.timeout ?? 0.0'), but the client never passed
it through, so local connections always opened with no busy handler.

Applying 'PRAGMA busy_timeout' manually doesn't stick either: since
transaction() hands the client's connection to the transaction and
lazily opens a fresh one for the next statement, the pragma is silently
lost after the first interactive transaction. Any later lock contention
(e.g. two processes sharing one database file) then fails instantly
with SQLITE_BUSY instead of waiting.

Passing the timeout through createClient() config applies it natively
to every connection the client opens, including the lazily-created
ones after transaction().

Fixes tursodatabase#288
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

busy_timeout is seemingly ignored (interleaving transactions will always fail)

1 participant