Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -300,28 +300,30 @@ export class TransactionManager {
}

async commitTransaction(transactionId: string): Promise<void> {
return await this.tracingHelper.runInChildSpan('commit_transaction', async () => {
await this.#withActiveTransactionLock(transactionId, 'commit', async (txw) => {
if (txw.depth > 1) {
if (!txw.transaction) throw new TransactionNotFoundError()
const savepointName = txw.savepoints.at(-1)
if (!savepointName) {
throw new TransactionInternalConsistencyError(
`Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`,
)
}
try {
await this.#releaseSavepoint(txw.transaction, savepointName)
} finally {
// Keep internal state consistent even if releasing the savepoint fails.
txw.savepoints.pop()
txw.depth -= 1
return await this.tracingHelper
.runInChildSpan('commit_transaction', async () => {
await this.#withActiveTransactionLock(transactionId, 'commit', async (txw) => {
if (txw.depth > 1) {
if (!txw.transaction) throw new TransactionNotFoundError()
const savepointName = txw.savepoints.at(-1)
if (!savepointName) {
throw new TransactionInternalConsistencyError(
`Missing savepoint for nested commit. Depth: ${txw.depth}, transactionId: ${txw.id}`,
)
}
try {
await this.#releaseSavepoint(txw.transaction, savepointName)
} finally {
// Keep internal state consistent even if releasing the savepoint fails.
txw.savepoints.pop()
txw.depth -= 1
}
return
}
return
}
await this.#closeTransaction(txw, 'committed')
await this.#closeTransaction(txw, 'committed')
})
})
})
.catch(rethrowAsUserFacing)
}

async rollbackTransaction(transactionId: string): Promise<void> {
Expand Down
11 changes: 11 additions & 0 deletions packages/client/tests/functional/issues/30580/_matrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMatrix } from '../../_utils/defineMatrix'
import { Providers } from '../../_utils/providers'

export default defineMatrix(() => [
[
{
driverAdapter: 'js_pg',
provider: Providers.POSTGRESQL,
},
],
])
20 changes: 20 additions & 0 deletions packages/client/tests/functional/issues/30580/prisma/_schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { idForProvider } from '../../../_utils/idForProvider'
import testMatrix from '../_matrix'

export default testMatrix.setupSchema(({ provider }) => {
return /* Prisma */ `
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "${provider}"
}

model AppSetting {
id ${idForProvider(provider)}
key String @unique
value String
}
`
})
61 changes: 61 additions & 0 deletions packages/client/tests/functional/issues/30580/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import testMatrix from './_matrix'
// @ts-ignore
import type { PrismaClient } from './generated/prisma/client'

declare let prisma: PrismaClient

testMatrix.setupTestSuite(
() => {
beforeEach(async () => {
await prisma.appSetting.deleteMany()
})

test('commit-time write conflicts are surfaced as P2034', async () => {
const prefix = 'probe-'
let reads = 0
let release!: () => void
const gate = new Promise<void>((resolve) => {
release = resolve
})

const run = (suffix: number) =>
prisma.$transaction(
async (tx) => {
await tx.appSetting.count({ where: { key: { startsWith: prefix } } })

// Release the barrier even if this transaction throws later, so the
// other transaction can never hang waiting on the gate.
reads += 1
if (reads === 2) release()
await gate

await tx.appSetting.create({
data: { key: `${prefix}${suffix}`, value: 'x' },
})
},
{ isolationLevel: 'Serializable' },
)

const results = await Promise.allSettled([run(1), run(2)])
const rejected = results.filter((r) => r.status === 'rejected')
const fulfilled = results.filter((r) => r.status === 'fulfilled')

expect(fulfilled).toHaveLength(1)
expect(rejected).toHaveLength(1)
expect(rejected[0]).toMatchObject({
reason: {
name: 'PrismaClientKnownRequestError',
code: 'P2034',
},
})

await expect(prisma.appSetting.count()).resolves.toBe(1)
})
},
{
optOut: {
from: ['mongodb', 'mysql', 'sqlite', 'sqlserver', 'cockroachdb'],
reason: 'Serializable write skew surfaced as SQLSTATE 40001 at COMMIT is the PostgreSQL behavior from the issue',
},
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.