question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

interactiveTransactions: 2 concurrent writes to the same row will cause it to hang until expiring

See original GitHub issue

Bug description

I’m trying to use interactive transactions to build a bank transfer. I’d like to try to achieve the following:

  • Alice and Bob have $100
  • Concurrently, Alice sends Bob $100 twice
    • One of those requests goes through
    • The other one is rejected saying there’s not enough money
  • Alice has $0, Bob has $100

How to reproduce

Reproduction Repo: https://github.com/matthewmueller/interactive-transactions

Given the following Prisma Schema

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["interactiveTransactions"]
}

model Account {
  id      Int    @id @default(autoincrement())
  email   String @unique
  balance Int
}

And the following script:

import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()

async function unseed() {
  await prisma.account.deleteMany({
    where: {
      OR: [{ email: "alice@prisma.io" }, { email: "bob@prisma.io" }],
    },
  })
}

async function seed() {
  await prisma.account.create({
    data: {
      email: "alice@prisma.io",
      balance: 100,
    },
  })
  await prisma.account.create({
    data: {
      email: "bob@prisma.io",
      balance: 100,
    },
  })
}

async function transfer(nth: number, from: string, to: string, amount: number) {
  return await prisma.$transaction(
    async (prisma) => {
      console.time("send " + nth)
      const sender = await prisma.account.update({
        data: {
          balance: {
            decrement: amount,
          },
        },
        where: {
          email: from,
        },
      })
      console.timeEnd("send " + nth)
      console.time("throw " + nth)
      if (sender.balance < 0) {
        throw new Error(`${from} doesn't have enough to send ${amount}`)
      }
      console.timeEnd("throw " + nth)
      console.time("recieve " + nth)
      const recipient = prisma.account.update({
        data: {
          balance: {
            increment: amount,
          },
        },
        where: {
          email: to,
        },
      })
      console.timeEnd("recieve " + nth)
      return recipient
    },
    {
      timeout: 20000,
    }
  )
}

async function main() {
  await prisma.$connect()
  await unseed()
  await seed()
  console.time("transfer")
  await Promise.all([
    transfer(1, "alice@prisma.io", "bob@prisma.io", 100),
    transfer(2, "alice@prisma.io", "bob@prisma.io", 100),
  ])
  console.timeEnd("transfer")
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect())

If you setup a database and then run ts-node index.ts, you’ll get:

send 1: 7.005ms
throw 1: 0.005ms
recieve 1: 0.279ms
send 2: 20.005s
throw 2: 0.367ms
recieve 2: 0.644ms
PrismaClientKnownRequestError3 [PrismaClientKnownRequestError]: 
Invalid `prisma.account.deleteMany()` invocation in
/Users/m/dev/src/github.com/prisma/interactive-transactions/index.ts:5:58

  2 const prisma = new PrismaClient()
  3 
  4 async function unseed() {
→ 5   await prisma.account.deleteMany(
  Transaction API error: Transaction already closed: Transaction is no longer valid. Last state: 'Expired'.
    at RequestHandler.request (/Users/m/dev/src/github.com/prisma/interactive-transactions/node_modules/@prisma/client/runtime/index.js:36361:15)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at PrismaClient._transactionWithCallback (/Users/m/dev/src/github.com/prisma/interactive-transactions/node_modules/@prisma/client/runtime/index.js:36932:18) {
  code: 'P2028',
  clientVersion: '2.30.0-dev.8',
  meta: {
    error: "Transaction already closed: Transaction is no longer valid. Last state: 'Expired'."
  }
}

Two notes:

  • The 2nd send takes the full 20s
  • An FYI that the stack trace is off. It was pointing to that line even when it was commented out.

I think this could be a bug because if I change the code to transfer serially, it works as expected:

async function main() {
  await prisma.$connect()
  await unseed()
  await seed()
  await transfer("alice@prisma.io", "bob@prisma.io", 100)
  await transfer("alice@prisma.io", "bob@prisma.io", 100) // Error: alice@prisma.io doesn't have enough to send 100
}

Expected behavior

I’d expect this to work as expected, you should be able to initialize multiple transactions concurrently and let the database sort it out.

Prisma information

Environment & setup

  • OS: OSX
  • Database: Postgres
  • Node.js version: v14.16.0

Prisma Version

Environment variables loaded from .env
prisma                : 2.30.0-dev.8
@prisma/client        : 2.30.0-dev.8
Current platform      : darwin
Query Engine (Binary) : query-engine 71d96e8bbd21982078694f00add0f51da2056a8b (at node_modules/@prisma/engines/query-engine-darwin)
Migration Engine      : migration-engine-cli 71d96e8bbd21982078694f00add0f51da2056a8b (at node_modules/@prisma/engines/migration-engine-darwin)
Introspection Engine  : introspection-core 71d96e8bbd21982078694f00add0f51da2056a8b (at node_modules/@prisma/engines/introspection-engine-darwin)
Format Binary         : prisma-fmt 71d96e8bbd21982078694f00add0f51da2056a8b (at node_modules/@prisma/engines/prisma-fmt-darwin)
Default Engines Hash  : 71d96e8bbd21982078694f00add0f51da2056a8b
Studio                : 0.419.0
Preview Features      : interactiveTransactions

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:7
  • Comments:16 (7 by maintainers)

github_iconTop GitHub Comments

11reactions
millspcommented, Jan 31, 2022
8reactions
MichalLytekcommented, Nov 9, 2021

@matthewmueller Any chance it will be fixed in v3.5.0? For now because of that, the interactive transactions are unusable in any http/graphql scenario when the requests can come at the same time 😕

Read more comments on GitHub >

github_iconTop Results From Across the Web

Transactions and batch queries (Reference) - Prisma
Nested writes: use the Prisma Client API to process multiple operations on one or more related records inside the same transaction.
Read more >
Transaction locking and row versioning guide - SQL Server
Lost updates occur when two or more transactions select the same row and then update the row based on the value originally selected....
Read more >
Prisma Interactive Transactions
To use the interactive transactions, you must enable it in your Prisma Schema ... 2 concurrent writes to the same row will cause...
Read more >
Do concurrent writes to the same table block each other?
Concurrent INSERT statements do NOT write to the same micro-partition, so there is no contention (that is, they do NOT block each other)....
Read more >
prisma/prisma 3.6.0 on GitHub - NewReleases.io
Prisma Client · interactiveTransactions: 2 concurrent writes to the same row will cause it to hang until expiring · Please support having in ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found