file: — SQLITE_BUSY-failed statement is never finalized; next transaction cannot commit and its abandoned BEGIN holds the write lock forever
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- sqlite, typescript
- 領域
- databases
調査の方向性
まず、ローカルファイル database に対して repro.mjs の例を実行し、次に client.transaction()、statement のエラー処理、tx.commit() を追跡します。失敗した statement と失敗した commit によって SQL ステートメントが実行中のままになったり、放棄された write lock が残ったりしなくなっていることを確認します。復旧用の書き込みを再実行し、明示的な rollback なしでデータベースが使用可能になることを確認します。
索引モデルが issue の本文から書いたものです。
説明
On a local file: database, a statement that fails with SQLITE_BUSY is never finalized on the client's shared native connection. The next transaction() on that client then fails at commit() with:
SQLITE_BUSY: cannot commit transaction - SQL statements in progress
and — because transaction() hands the current native connection to the transaction and nothing ever closes it (#350) — if the caller does not explicitly roll back, the open BEGIN holds the database write lock until the process exits. Every subsequent write from any connection fails SQLITE_BUSY immediately.
This is the mechanism behind two production outages for us (the caller was @prisma/adapter-libsql, which never issues a successful rollback after a failed commit — filed as prisma/prisma#30028 — but the underlying behavior is reproducible with @libsql/client alone).
Two findings from the repro below:
- The priming step is just one transient
SQLITE_BUSY(in production: a Litestream checkpoint briefly holding the write lock). The client looks healthy afterwards; the corruption only surfaces at the next transaction's commit. Likely the same statement-finalization gap as tursodatabase/libsql-js#228. - An explicit
rollback()after the failed commit does succeed and releases the lock — so the state is recoverable, but only if the caller knows to do this. Callers that treat a failed commit as "transaction closed" (a reasonable assumption, and what Prisma's engine does) leak the lock permanently.
repro.mjs — @libsql/client 0.17.4, Node 24, needs `sqlite3` CLI on PATH
import { execFile } from 'node:child_process'
import { createClient } from '@libsql/client'
const client = createClient({ url: 'file:./repro2.db' })
await client.execute('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)')
await client.execute('PRAGMA journal_mode = WAL')
await client.execute("INSERT OR REPLACE INTO t (id, v) VALUES (1, 'base')")
const write = async (label) => {
try {
await client.execute({ sql: 'UPDATE t SET v = ? WHERE id = 1', args: [label] })
console.log(`${label}: OK`)
} catch (e) {
console.log(`${label}: FAIL ${String(e.message).slice(0, 80)}`)
}
}
// 1. Prime: a write fails SQLITE_BUSY while another process holds the lock.
const holder = new Promise((res) =>
execFile('bash', ['-c', `(echo "BEGIN IMMEDIATE; UPDATE t SET v='held' WHERE id=1;"; sleep 2; echo "COMMIT;") | sqlite3 repro2.db`], () => res()),
)
await new Promise((r) => setTimeout(r, 400))
await write('during-lock') // FAIL SQLITE_BUSY — the priming statement
await holder
await write('after-release') // OK — client looks healthy
// 2. Transaction with concurrent writes on the same client.
const tx = await client.transaction('deferred')
try {
await tx.execute("UPDATE t SET v = 'itx' WHERE id = 1")
await Promise.all([write('concurrent-1'), write('concurrent-2')])
await tx.commit()
console.log('tx: committed')
} catch (e) {
console.log(`tx commit: FAIL ${String(e.message).slice(0, 90)}`)
// NOTE: uncommenting this recovers — but a caller that assumes a failed
// commit closed the transaction (e.g. Prisma) leaks the write lock forever:
// await tx.rollback()
}
// 3. Recovery probes — all fail until the process exits.
for (let i = 1; i <= 4; i++) {
await new Promise((r) => setTimeout(r, 700))
await write(`recovery-${i}`)
}
process.exit(0)
Output (abandoned-tx variant):
during-lock: FAIL SQLITE_BUSY: database is locked
after-release: OK
concurrent-1: FAIL SQLITE_BUSY: database is locked
concurrent-2: FAIL SQLITE_BUSY: database is locked
tx commit: FAIL SQLITE_BUSY: cannot commit transaction - SQL statements in progress
recovery-1: FAIL SQLITE_BUSY: database is locked
recovery-2: FAIL SQLITE_BUSY: database is locked
recovery-3: FAIL SQLITE_BUSY: database is locked
recovery-4: FAIL SQLITE_BUSY: database is locked
Suggested fixes, any of which would break the chain: finalize failed statements before returning the error; make commit() roll back + close the detached connection when it fails with SQL statements in progress; or close the detached connection when the Transaction object is GC'd (#350 / libsql-js#230 territory).
- 主要言語
- TypeScript
- スター
- 578
- フォーク
- 70
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
tursodatabase/libsql-client-ts のほかの issue
-
SqlCache measures SQLD's 5 KB stored-SQL cap in UTF-16 code units, but the server enforces it in UTF-8 bytes対応中かも @OsamaAnsar が 66 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
tursodatabase/libsql-client-ts#353 · コメント 1 件 ·
-
`concurrency: 0` is documented to disable the limit, but expandConfig turns it into 20再び着手できるかも このイシューのプルリクエストはマージされずにクローズされました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
-
Local file: a COMMIT that fails with SQLITE_BUSY keeps the database write-locked until the process exits対応中かも @just-some-random-pal が 25 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 52/100
-
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
tursodatabase/libsql-client-ts#354 · コメント 1 件 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
tursodatabase/libsql-client-ts#350 · コメント 3 件 ·
tursodatabase/libsql-client-ts の issue をすべて見る
似ている issue
-
documentation good first issue roadmap
難易度 1/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
resend/react-email#3876 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
siyuan-note/siyuan#21324 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
dmno-dev/varlock#1279 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
himynameisdave/bsky-profanity-counter#59 ·
メンテナーはふだん 1 日以内に返信