prisma: WriteConflict error with MongoDB
Bug description
lib/prisma.js -
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient({log: ['query', 'info', 'warn']})
module.exports = {
prisma
}
Using MongoDB free atlas tier, just created database and just connected but getting an error named WriteConflict
12 if(!totalMessages[0]){
→ 13 await prisma.Statistics.create(
Error in connector: Database error. error code: unknown, error message: Command failed (WriteConflict): WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.)
Last logs
[14/04/2022 06:17-info] Application started:username[[ SOS ]#9847]
prisma:query db.Statistics.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$type", "TOTAL_MESSAGES", ], }, { $or: [ { $ne: [ { $ifNull: [ "$type", null, ], }, null, ], }, { $eq: [ "$type", null, ], }, ], }, ], }, }, }, { $project: { _id: 1, type: 1, total: 1, date: 1, }, }, ])
prisma:query db.Statistics.aggregate([ { $match: { $expr: { $and: [ { $and: [ { $eq: [ "$type", "DAILY_MESSAGES", ], }, { $or: [ { $ne: [ { $ifNull: [ "$type", null, ], }, null, ], }, { $eq: [ "$type", null, ], }, ], }, ], }, { $and: [ { $eq: [ "$date", "14-04-2022", ], }, { $or: [ { $ne: [ { $ifNull: [ "$date", null, ], }, null, ], }, { $eq: [ "$date", null, ], }, ], }, ], }, ], }, }, }, { $limit: 1, }, { $project: { _id: 1, type: 1, total: 1, date: 1, }, }, ])
prisma:query db.UserStatistics.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$userId", "788989831921139722", ], }, { $or: [ { $ne: [ { $ifNull: [ "$userId", null, ], }, null, ], }, { $eq: [ "$userId", null, ], }, ], }, ], }, }, }, { $project: { _id: 1, userId: 1, total: 1, firstMessage: 1, }, }, ])
prisma:query db.Users.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$userId", "788989831921139722", ], }, { $or: [ { $ne: [ { $ifNull: [ "$userId", null, ], }, null, ], }, { $eq: [ "$userId", null, ], }, ], }, ], }, }, }, { $limit: 1, }, { $project: { _id: 1, userId: 1, username: 1, avatarURL: 1, firstMessageDate: 1, }, }, ])
prisma:query db.Statistics.insertOne({ type: "TOTAL_MESSAGES", total: 1, date: "14-04-2022", })
prisma:query db.Users.insertOne({ userId: "788989831921139722", username: "schwarzsky#8251", avatarURL: "https://cdn.discordapp.com/avatars/788989831921139722/8ccd47aa5de6579290ed3c0f5897d709.webp", firstMessageDate: "14/04/2022 06:17", })
prisma:query db.Statistics.aggregate([ { $match: { $expr: { $and: [ { $and: [ { $eq: [ "$_id", "6257923be618d92a90c2454b", ], }, { $or: [ { $ne: [ { $ifNull: [ "$_id", null, ], }, null, ], }, { $eq: [ "$_id", null, ], }, ], }, ], }, ], }, }, }, { $project: { _id: 1, type: 1, total: 1, date: 1, }, }, ])
prisma:query db.Users.aggregate([ { $match: { $expr: { $and: [ { $and: [ { $eq: [ "$_id", "6257923be618d92a90c2454c", ], }, { $or: [ { $ne: [ { $ifNull: [ "$_id", null, ], }, null, ], }, { $eq: [ "$_id", null, ], }, ], }, ], }, ], }, }, }, { $project: { _id: 1, userId: 1, username: 1, avatarURL: 1, firstMessageDate: 1, }, }, ])
prisma:query db.Statistics.insertOne({ type: "DAILY_MESSAGES", total: 1, date: "14-04-2022", })
[14/04/2022 06:17-info] registered:ID[788989831921139722] username:[schwarzsky#8251]
prisma:query db.UserStatistics.insertOne({ userId: "788989831921139722", total: 1, firstMessage: "2022-04-14T03:17:15.566+00:00", })
prisma:query db.Statistics.aggregate([ { $match: { $expr: { $and: [ { $and: [ { $eq: [ "$_id", "6257923be618d92a90c2454d", ], }, { $or: [ { $ne: [ { $ifNull: [ "$_id", null, ], }, null, ], }, { $eq: [ "$_id", null, ], }, ], }, ], }, ], }, }, }, { $project: { _id: 1, type: 1, total: 1, date: 1, }, }, ])
prisma:query db.UserStatistics.aggregate([ { $match: { $expr: { $and: [ { $and: [ { $eq: [ "$_id", "6257923be618d92a90c2454e", ], }, { $or: [ { $ne: [ { $ifNull: [ "$_id", null, ], }, null, ], }, { $eq: [ "$_id", null, ], }, ], }, ], }, ], }, }, }, { $project: { _id: 1, userId: 1, total: 1, firstMessage: 1, }, }, ])
[14/04/2022 06:17-info] created:DAILY_MESSAGES:14-04-2022
[14/04/2022 06:17-info] created:USER_TOTAL:ID[788989831921139722]
How to reproduce
Expected behavior
Must create an row with type:TOTAL_MESSAGES
.
Prisma information
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model Users {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @unique
username String
avatarURL String
firstMessageDate String
}
model StaffTags {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
fullMessage String
type StaffType[]
}
model Statistics {
id String @id @default(auto()) @map("_id") @db.ObjectId
type StatisticType
total Int
date String
}
model UserStatistics {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @unique
total Int
firstMessage DateTime @default(now())
}
an basic event that explains all my code.
const { GetToday } = require("../GetToday")
const { prisma } = require("../prisma")
const { logger } = require("../winston")
const registerTotalMessagesEvent = async () => {
const totalMessages = await prisma.Statistics.findMany({
where: {
type: 'TOTAL_MESSAGES'
}
})
if(!totalMessages[0]){
await prisma.Statistics.create({
data: {
type: 'TOTAL_MESSAGES',
total: 1,
date: GetToday()
}
})
logger.info('created:TOTAL_MESSAGES')
} else {
await prisma.Statistics.updateMany({
where: {
type: 'TOTAL_MESSAGES'
},
data: {
total: totalMessages[0].total + 1
}
})
logger.info(`updated:TOTAL_MESSAGES[total=${totalMessages[0].total + 1}]`)
}
}
module.exports = {
registerTotalMessagesEvent
}
Environment & setup
- OS: Windows11 64 bit
- Database: MongoDB
- Node.js version: 16.14.0
Prisma Version
3.12.0
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 14
- Comments: 87 (18 by maintainers)
Hey folks,
With the lack of reports on write conflict errors ever since we’ve implemented retries, we will close this issue now. Thanks for the feedback and for bearing with us until we got it implemented y’all 🙏
Hey folks,
Starting in Prisma 4.9.0, all queries except interactive transactions are now automatically retried like MongoDB drivers do.
To be clear, what won’t be retried automatically are queries such as this one:
We hope that this will already solve most of your problems.
Thanks 🙏
if anybody needs it for nestjs prisma
Credit to @aladinflux
Hey folks, thanks for keeping this thread alive. We’re currently working on this and would like your feedback on something.
While “regular” queries are safe to be retried automatically, we fear that automatic retries for interactive transactions might lead to undesirable behaviors.
See, while it’s not recommended to perform side-effects in a transaction callback, we know many users are doing it.
The consequence of performing silent, automatic retries on non-idempotent side-effects might be bad and surprising for many.
Now, after reviewing what most MongoDB drivers do, we’ve noticed that they all seem to be performing those automatic retries.
Here are two questions for you:
There are the few options we have (keep in mind we’re specifically talking about ITXs):
Based on your experience, let us know what you’d prefer.
(Please keep in mind that those API design decisions are always full of trade-offs and that we may respectfully have to choose a different direction than what y’all would like. Your feedback remains very valuable nonetheless)
Thanks for your help, cheers 🤓
Thanks for the continued feedback here.
As some of you already figured out, we were not MongoDB experts when we started our implementation and seem to have gotten some things not perfectly right - especially compared to other MongoDB drivers and libraries. As someone also pointed out, we come from a different direction in how we treat schema, data and relations, so this decision made sense to use at the time.
We are now investigating what exactly we are doing why, and what and how we can change this to minimize the impact of this error and optimally just get rid of it.
PS: We indeed use the official MongoDB Rust driver as @codeinearts mentioned.
Switched to Mongoose, because of this error and haven’t looked back. It works like a charm and is a simple technology compared to Prisma (Not as many moving parts). Albeit it does not have as many fancy features as Prisma.
But in my experience it is plenty sufficient.
I can reproduce the problem with 3.12 but with 4.10 we throw the following error when generating the client:
But the original problem is still reproducible even after fixing the types:
So we’ve already implemented that strategy as I mentioned above but with high throughput documents will still eventually trigger it - MongoDB’s non-transaction behaviour (Put updates in a queue) would be ideal for these situations as in our situation we are just using atomic updates to increment / decrement a user’s credits.
In these cases we had to re architecture our code to have a queue to process these in a way to avoid conflicts all together. So some way to say to the client “hey we actually want this up date to be ran outside of a mongo db transaction” would be ideal.
We haven’t seen the WriteConflict error since upgrading to 4.9.0 to. Good work 🙌
The way I solved this issue is by using a middleware that catches WriteConflict issues thrown by prisma and retries the write using the
retry
npm package to keep retrying the transaction for 5 times with different intervals until it succeeds and I’m happy to say that we got rid of all WriteConflict issues we were having.You can try disconnecting prisma instance every time after the write operation is done. It is working for me.
From this
To this.
Notice that it could affect writing performance. But I did not benchmark it.
We can recreate this error with atomic updates which are supposed to be to avoid WriteConflicts and race conditions.
Given the following API call:
We will get a WriteConflict from MongoDB if two
update
calls are triggered at the same time. The expected behaviour is that thecredits
field would be incremented by MongoDB for each call without any WriteConflicts but unfortunately it throws an error.I believe this is because Prisma is internally is using Transactions with MongoDB which fails whenever two clients touch the same document no matter if it’s using atomic updates or not.
TL;DR; We migrated all our db operations to Mongoose as a solution and did not receive a single WriteConflict. Also as a welcome side effect, db operations started taking a lot shorter.
I couldn’t find a solution to this with Prisma and unfortunately it was a mess when we went live and users started coming. Although prisma says that they support Mongo, the level of support is far from what anyone would need in production. RDBMS support could be better, but I don’t know I haven’t tried it.
The main issue here is,
I think, that Prisma uses their own driver instead of using mongo’s own driver.(I stand corrected by @codeinearts, prisma uses official mongo drive for Rust, which in contrast with official node.js driver, uses transactions for everything. See his actual comment here: https://github.com/prisma/prisma/issues/12814#issuecomment-1339376488 ) And it’s a mistake on our side to choose Prisma over other options, as they didn’t claim to have 100% feature parity with official node.js mongo driver.From what I understand, and to answer your first question, yes, that should mean another request handler was at that moment interfering. This might or might not be desired:
a) if two users edit a document title simultaneously, you might NOT WANT to reflect the latest update if the last one didn’t finished
b) if two users add one like to a document, you might not care of the order as long as the sum is correct
In the case ‘b’ a writeConflict would be very prone to happen if not handled atomically and correctly (see https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#atomic-number-operations)
As per your second question, is made like that by design and I have no idea, tho I think is kind of a rule of thumb to avoid certain scenarios like case ‘a’ mentioned above. Also, from a control point of view, It also does make sense to have visibility of the actual ‘underlying error’ instead of having an ‘automagically’ retry logic doing the work for us - although, highly desired IMHO -
As per the root cause, the reason is mongodb driver automatically handles retries for us in most cases, except when transactions are involved. As @Jolg42 shared above, this video explains the behaviour of write conflicts at mongodb very well https://www.youtube.com/watch?v=3x_Pf9rQGCo&t=723s
Since prisma uses transactions by default on everything, we get this ‘writeConflict’ error bubble up to end up slapping our faces
In your particular case, I’d not worry too much. However, you could try https://www.prisma.io/docs/concepts/components/prisma-client/transactions if needed. I’m not sure how it is implemented, but it might enclose the underlying error with some retry logic or similar - Although, overkill IMHO -.
about prisma engine
Prisma uses something called
Tokio runtime
, which is basically a Rust package made to efficiently handle async I/O (kind of JS V8 on steroids lol). That uses then Rust’s mongodb driver behind the scenes to make the actual connection to the database, which apparently neither does implement auto retries for transactions (nodejs mongodb driver implement that usingsession.withTransaction
)This behaviour could led to other possible potential issues when dealing for example with serverless databases like the one mongodb offers at atlas, as it depends on its retry behaviour in case of sockets close
More on mongodb retry behaviour can be read here https://www.mongodb.com/docs/manual/core/retryable-writes
@jcampbell05 MongoDB recommends retrying those transactions until they succeed. Their MongDB driver on NodeJS does this automatically using
session.withTransaction
. If Prisma copies this logic and retries the transactions, it could solve the problem. https://www.mongodb.com/docs/manual/core/retryable-writes/This is still a huge issue for us, and it’s very consistent; updating the same model multiple times in a short period of time causes this error
Transaction failed due to a write conflict or a deadlock. Please retry your transaction
we are stuck with this issue for now, is there a workaround or an option with work with Prisma without enabling replica set?@Weakky Amazing we will give a go 😀 I think the only thing left would be smart queuing of updates which only do atomic updates.
In this situation for high volume workloads a retry we just cause requests to back up. We had to re-architected our application to avoid this by queuing updates based on which document is being updated but that it overkill for simple applications.
Since MongoDB already queues updates outside of a transaction maybe it may be worth allowing us to update a document outside of a transaction. Only if we are using atomic updates.
Mongo’s current behaviour with Prisma currently results in a conflict error if two atomic updates touch the same document within a transaction. This is to stop a document being left in an inconsistent state but the whole purpose of atomic updates is that they can touch the same document and it will be updated correctly without leaving it in an inconsistent state.
Prisma could implement retries by default as this is just adding extra code to write if using Prisma 😗
That is expected for upserts and can be handled by retrying the query: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#remarks-8 This is a different problem from what this issue here is about though @luluhoc
We can confirm this issue. We sometimes receive the below error. In our case we think that happens when the same object is searched and updated for at least twice in a very short amount of time. It could also be related to stopping of the current (Prisma update) request and leaving it in a unfinished (bad?) state through a 2nd request (new one) from the same client.
This WriteConflict issue is really hitting us hard. We went to production with Prisma and we keep getting these issues and it’s extremely difficult to debug or find out about what caused the WriteConflict.
We have a simple code:
It simply fetches the token, then updates the user and clears the token. Prisma fails at the user update with a
WriteConflict
error.If only prisma provided more context about the issue or would internally retry a failed transaction a couple of times before failing.
Have you ever been bitten by those automatic retries while using Mongoose or any Mongo drivers? No we didn’t have any issues, we’re using MongDB in production (driveflux.com) and we didn’t face any issues. The solution I posted above worked for us and all WriteConflict issues disappeared. We didn’t notice any bad side effects.
Also, have you ever needed to disable those automatic retries? Not at all
Automatic retries enabled by default (put behind a preview feature flag until we GA it) Yes
Thank you @Weakky
Yeah, sorry I thought I responded with quote, I’ll edit the comment.
Yes, this is how we use Typescript with Mongoose. It is quite effective:
It makes any operation with the model conform to the proper types.
I believe that Prisma must retry the transaction a couple of times before failing. That’s what the documentation in MongoDB says.
A workaround is to use a try catch and retry the query, however, it’ll be horrible to wrap every write with a try catch throughout the code.
Alright dpetrick, so after switching from
Promise.all
to afor
loop, I didn’t get theWriteConflict
issue. But this is a bug in my opinion. Primsa should be able to handle this situation.iam facing this issue, i did a simple for loop for up to 1000 promises, and do a promise.all , i get deadlock error. i did not use any transaction and this is a mongodb driver. using “@prisma/client”: “^5.0.0”,
@janpio hard to say as our mitigations are in place. But we haven’t seen it in a while.
Most of our remaining issues are random issues such as unexpected hangups and SSL issues with the hosted Mongo which I’m not sure are an issue with Prisma or just the random issues you get at scale.
Have you ever been bitten by those automatic retries while using Mongoose or any Mongo drivers? Never when using mongoose
Also, have you ever needed to disable those automatic retries? no
Automatic retries enabled by default (put behind a preview feature flag until we GA it) Yes
Thanks @Weakky
Never
No
Yes thank you
@emrah-b Not a prisma.io evangelist here, but some of what you state is wrong: prisma actually uses Rust’s official mongodb’s native driver, and that can be seen here https://github.com/prisma/prisma-engines/blob/main/query-engine/connectors/mongodb-query-connector/Cargo.toml
The problem is that it is using transactions for everything, which end up giving writeConflicts for some dead simple operations, which is the correct behaviour of mongodb’s native driver when using transactions.
Those “dead simple operations” would benefit a lot of the regular behaviour we all mongo db users native driver are used to: mongoclient to automatically retrying the queries for us.
Also, and I’ll agree in that’s sadly something true, is that in order to keep consistency with their api among all other dbms, it forces mongodb to use aggregate pipelines - kind of joins on mongo lol - and transactions, which could be the reason you’re experiencing a difference on performance compared to mongoose - after all, on most of the writes with mongoose you’re probably not having the overhead of aggregation pipelines -.
I have gotten this error in one endpoint of an express application. Happened for a simeple
prisma.<model>.update()
call.The express handler basically looks like this
so I can’t imagine the conflict coming from within this handler. Does this mean another request handler must be interfering in an asynchronous way? Does anyone know why prisma enforces transactions for simple updates?
@Kondamon James (@jcampbell05) is also a user of Prisma and MongoDB, who implemented that themselves. This is currently not included in Prisma.
@jcampbell05 Thanks! I need to organize some thoughts first about what this is about exactly. So we have a better understanding of it, from looking at all the comments it’s very much needed!
Early thought (work in progress): from a “first look”, it looks like it’s about how MongoDB works with transactions/writes, and the behavior is different depending on if you use writes and writes inside a transaction. (Prisma always uses a transaction)
https://www.mongodb.com/docs/manual/core/transactions-production-consideration/#in-progress-transactions-and-write-conflicts
This video explains it very well https://www.youtube.com/watch?v=3x_Pf9rQGCo&t=723s
@Jolg42 let me know if there is anything I can do to help you with this issue
apparently this still not solved … switching to mongoose
This error occurred when I ran a script to migrate data from my old database to MongoDB. Writing data + Promise.all will 100% cause it. The version I use is 4.1.1.
P/s: I replaced
Promise.all
withprisma.$transaction
then the script is working like a charm now.I have fixed this issue as well, the solution was to use MongoDB to create the collections before seeding.
The problem is, when the database is empty and prisma has to seed data and create the collections at the same time, it will result in
WriteConflict
error, so here’s my solution:If you can help us to get to a reproduction with that new knowledge, we will certainly take a look.
I start to suspect it’s
Promise.all
. The modifiedmitt
package does aPromise.all(handlers)
which is not running handlers serially and maybe 2 queries fire at the same time.I have a DateTime field in my schema. If I comment out updating this field, the write errors stop. I’ve tried passing an ISO date string, and a JS date/time object. The errors are intermittent until I comment out this field. I’m just inserting 100 records at a time. I’ve tried each insert as a separate transaction, and not.
Also, @janpio , if I remove the
deleteMany
part, then it works unreliably. After multiple times re-running the seeder, it worked. So for every ~20 times running the command, one will work.I expect we will face this issue with the migration of our data (not schema migration) as well. We’re going to migrate from our old database to a new DB with a new structure soon, and the migration script will run in a similar fashion.
So “creating multiple seeders” just means that I added more “seedXXX” functions.
Here’s a reproduction repo:
https://github.com/aladinflux/prisma-conflit-issue
The seeder code is inside
prisma
folder.Just run
npx prisma db seed
after setting theDATABASE_URL
env.Thanks for the additional information. Can you share the connected schema so we have an easier time to run this ourselves? What exactly do you mean by “After creating multiple seeders, when we run this script on terminal, we …” - what are the exact steps for us to reproduce what you observe?