-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix(sqlite): serialize interactive transactions in-process to prevent worker thread starvation (#29870) #30120
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
base: main
Are you sure you want to change the base?
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -81,4 +81,38 @@ describe('sqlite transaction()', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await expect(db.transaction(async () => 'value')).rejects.toThrow('SQLite client is closed'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('concurrent transaction() calls are serialised and all succeed (#29870)', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Regression test: before the fix, launching more concurrent transactions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // than libuv worker threads caused SQLITE_BUSY to propagate as a P1008 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // socket-timeout error because the busy-handler blocked worker threads | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // while the lock holder's COMMIT had no thread to run on. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // With the async semaphore in place, each transaction waits its turn in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the Node.js event loop instead of inside the synchronous busy-handler, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so all of them complete successfully regardless of concurrency. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const db = sqlite({ contract, path: ':memory:' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await db.connect({ path: ':memory:' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const N = 20; // well above typical libuv thread-pool size (4 by default) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const order: number[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const results = await Promise.allSettled( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Array.from({ length: N }, (_, i) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.transaction(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| order.push(i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return i * 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // All transactions must have resolved (none rejected). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fulfilled = results.filter((r) => r.status === 'fulfilled'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(fulfilled).toHaveLength(N); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The serialisation guarantee: every transaction ran exactly once. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(order).toHaveLength(N); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+114
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert the serialization invariant. The Track active callbacks across an Proposed test change- const N = 20; // well above typical libuv thread-pool size (4 by default)
+ const transactionCount = 20;
const order: number[] = [];
+ let activeTransactions = 0;
+ let maxActiveTransactions = 0;
const results = await Promise.allSettled(
- Array.from({ length: N }, (_, i) =>
+ Array.from({ length: transactionCount }, (_, i) =>
db.transaction(async () => {
- order.push(i);
- return i * 2;
+ activeTransactions += 1;
+ maxActiveTransactions = Math.max(maxActiveTransactions, activeTransactions);
+ try {
+ await Promise.resolve();
+ order.push(i);
+ return i * 2;
+ } finally {
+ activeTransactions -= 1;
+ }
}),
),
);
const fulfilled = results.filter((r) => r.status === 'fulfilled');
- expect(fulfilled).toHaveLength(N);
+ expect(fulfilled).toHaveLength(transactionCount);
+ expect(maxActiveTransactions).toBe(1);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await db.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: prisma/prisma
Length of output: 26503
🏁 Script executed:
Repository: prisma/prisma
Length of output: 50369
🏁 Script executed:
Repository: prisma/prisma
Length of output: 12801
🏁 Script executed:
Repository: prisma/prisma
Length of output: 18535
🏁 Script executed:
Repository: prisma/prisma
Length of output: 13936
Share the transaction queue across clients for the same SQLite database.
sqlite()creates a separatetransactionQueueTailandSqliteDriverper client.SqliteDriver.acquireConnection()opens a separateDatabaseSyncfor the supplied path. Therefore, two clients using the same file can enterBEGIN…COMMITconcurrently and may reintroduce theSQLITE_BUSY/P1008starvation condition. Key a process-local queue by canonical database identity, or document and test that serialization applies only within one client.🤖 Prompt for AI Agents