Repository navigation
fix(shared): opening a database waits for another process's lock #28288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ import * as FileSystem from "effect/FileSystem"; | |
| import * as Path from "effect/Path"; | ||
| import * as SqlClient from "effect/sql/SqlClient"; | ||
| import * as Statement from "effect/sql/Statement"; | ||
| import { ChildProcess, ChildProcessSpawner } from "effect/process"; | ||
| import * as Stream from "effect/Stream"; | ||
| import type * as Tracer from "effect/Tracer"; | ||
| import { vi } from "vite-plus/test"; | ||
|
|
||
|
|
@@ -192,6 +194,34 @@ it.effect("keeps another connection from committing between a transaction's read | |
| }).pipe(Effect.provide(NodeServices.layer)), | ||
| ); | ||
|
|
||
| it.effect("waits for another process's lock while the database opens", () => | ||
| Effect.gen(function* () { | ||
| const fs = yield* FileSystem.FileSystem; | ||
| const path = yield* Path.Path; | ||
| const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-open-" }); | ||
| const filename = path.join(directory, "state.sqlite"); | ||
| const seed = new NodeSqlite.DatabaseSync(filename); | ||
| seed.exec("PRAGMA journal_mode = WAL; CREATE TABLE entries(name TEXT NOT NULL)"); | ||
| seed.close(); | ||
| const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||
| const holder = yield* spawner.spawn( | ||
| ChildProcess.make(process.execPath, [ | ||
| "-e", | ||
| `const db = new (require("node:sqlite").DatabaseSync)(process.argv[1]); | ||
| db.exec("PRAGMA locking_mode = EXCLUSIVE; BEGIN EXCLUSIVE"); | ||
| console.log("held"); | ||
| setTimeout(() => db.close(), 300);`, | ||
| filename, | ||
| ]), | ||
| ); | ||
| yield* Stream.runHead(holder.stdout); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require the child’s readiness signal. If the child exits before acquiring the lock, its stdout can be empty.
|
||
| yield* Effect.gen(function* () { | ||
| const sql = yield* SqlClient.SqlClient; | ||
| assert.deepEqual(yield* sql`SELECT COUNT(*) FROM entries`.values, [[0]]); | ||
| }).pipe(Effect.provide(SqliteClient.layer({ filename, checkpointWorker: true }))); | ||
| }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | ||
| ); | ||
|
|
||
| it.effect("reports a transaction blocked by another writer as a lock timeout", () => | ||
| Effect.gen(function* () { | ||
| const { filename, other } = yield* makeTempDatabase; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the lock until the open attempt is confirmed.
If the parent is delayed for 300 ms after the readiness message, the child releases the lock before the client opens. The test can then pass on the unfixed code. Replace the timer with coordination that confirms the open attempt occurs while the lock is held.
As per coding guidelines, “A test that needs a timeout to pass is wrong.”
Source: Coding guidelines