prisma: upsert across HTTP requests has a race condition
Bug description
Situation: I have a model with a unique string. At the beginning I have an empty database. I want to create or update an object of the model with upsert and check if the object exists by searching for the unique string.
If I call several upserts with the same unique string at the same time, I expect it to be created once and then updated if necessary. But it is created once and then the error occurs: Unique constraint failed on the fields.
Unique constraint failed on the fields: (`testId`).
How to reproduce
Expected behavior
I expect it to be created once and then updated the just created object, instead of crashing.
Prisma information
model Test {
id Int @default(autoincrement()) @id
testId String @unique
}
const prismaClient = new PrismaClient({
log: ["info", "warn", "error"],
});
const upsertPromises: Promise<Test>[] = [];
for (let i = 0; i < 2; i++) {
upsertPromises.push(
prismaClient.test.upsert({
where: { testId: "testId" },
create: {
testId: "testId",
},
update: {},
})
);
}
await Promise.all(upsertPromises);
Environment & setup
- OS: Mac OS,
- Database: PostgreSQL
- Node.js version: v13.3.0
- Prisma version: 2.3.0
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 29
- Comments: 43 (16 by maintainers)
Commits related to this issue
- Add remark about upsert’s race conditions https://github.com/prisma/prisma/issues/3242 — committed to dtinth/docs-2 by dtinth 2 years ago
Should Prisma’s
upsertnot being a real upsert be highlighted by the documentation? Or alternatively the method renamed tocreate_or_updatelike in ActiveRecord. 🤔 It is quite the blocker when you start getting unique constraint errors from simple upserts.Prisma’s data guide pages even describe the real upsert so Google results can get quite confusing.
Who though that doing completely opposite what the DataGuide recommends is a good choice?
I understand rationale for supporting other mechanism that do not support native UPSERT, but then just make it clear in Docs, and underneath use whatever is best suited for each type of DB. You are using ForeignKeys in Postgres, even though MongoDB doesn’t have it right?
Ok, so let me learn, but then do NOT allow me to use the full potential of the DB, at the same time misleading me of what upsert does (dataguide says to use prisma’s upsert as example of how to use postgre’s UPSERT, which is a lie). What gives? You are teaching people to do X, and at the same time you do not do the X. You even make it hard/impossible to do the correct thing while using Prisma 😦
Thanks @henrikuper, that’s a great point about upsert across requests being racy.
Highlighting @Dremora’s SQL to show that if two requests come in at the same time trying to update the same keys, you can end up with a unique constraint violation.
This happens in two transactions (T1 & T2) running at the same time.
alice@prisma.io. Not found, we’re creating.alice@prisma.io. Not found, we’re creating.alice@prisma.iointo the DB. All good.alice@prisma.iointo the DB. Unique constraint violation. Rollback.The solution is to use the upsert capabilities of the database itself, which turn these separate operations into one operation:
alice@prisma.io. Not found. Insertalice@prisma.iointo the DB. All good.alice@prisma.io. Found. Updatealice@prisma.iointo the DB. All good.You may wonder why the second example finds Alice this time. I believe this is because operations that can write to the table need to be serialized. Since we know that this operation is an upsert, T2 will wait until T1 releases the lock on the table.
I’m also experiencing this issue when attempting to idempotently create records:
As others have stated if you call this multiple times the first call will succeed and subsequent calls will fail:
Thanks @matthewmueller for the three ways workarounds, which sound very good if you have one of the three cases.
Actually I have a 4. alternative which causes this error, which can not be solved with one of these three solutions.
-> api endpoint is called twice at the exact same time with the same unique key.
This happens nearly never in reality but theoretically it can happen. Both calls trigger the upsert function with the same unique key. Now the race condition read read create create happens and an error is thrown.
I solved the issue with a dirty hack by
This doesn’t completely solve the issue, but reduces the probability of occurrence enough that there are no problems in the real application.
I mentioned data guide because directly underneath the information about how to use/code ON CONFLICT in Postgres clause there is a mention
which links to prisms docs. It is highly misleading but I still think prisma in Postgres should just use ON CONFLICT clause. Just as data guide suggest.
Hey folks, this is expected behavior.
If you issue two upserts concurrently in the Prisma Client, under the hood we do a read, then a write. This is to support data sources that don’t have native upsert support.
Unfortunately, this means that your concurrent queries have a race condition and may look like this: read-read-create-create, and in this case, the second create fails the constraint.
A couple workarounds:
It looks like you’re just trying to create many records at once anyway, so you can use our createMany API with
skipDuplicates: true. Under the hood this does anON CONFLICT DO NOTHINGin Postgres.You can also use upsert with await:
@Dremora I can reproduce the above issue. We should fix that.
I got bitten by this too. I submitted a documentation change to add the following remark to
upsert’s API docs:PR:
@garrensmith thanks for adding real upserts – super helpful. Any reason you’re not supporting MySQL
ON DUPLICATE KEY, or plans to in the future? We (MySQL users) want true upserts too, it shouldn’t be much different thanON CONFLICTWe’re running into this as well, with more or less exactly the case described here, where a user causes two nearly simultaneous http requests, which both attempt to “upsert” a user.
What’s especially bugging me is that we already have a retry middleware in place, but that simply keeps retrying the underlying “create”, hence always failing.
Our current workaround is to
awaitthese calls, catch any conflicts and (if there occurs one) fetch and return the persisted user instead. However, this doesn’t sound like something a framework user should need to implement for anupsert.👋 Any update here? I also ended up hacking together a try-catch solution like @henrikuper but it’s very gross.
I’m not an expert in SQL, but below are potential solutions. One would be to use
INSERT INTO ... ON CONFLICTstatement, that is basically an upsert. It only exists in Postgres.UPSERTcan be used in MySQL instead.An example from https://www.postgresqltutorial.com/postgresql-upsert/:
Any unique index key can be used in the
ON CONFLICTclause.Alternatively, database table would have to be locked using
ACCESS EXCLUSIVEfor reads before doing the firstSELECT. This would ensure that theSELECT/INSERTpair is atomic. However, locking the whole table is not great for performance.Experiencing the same problem with a compound key… I too believe something should be done about it. Having to use the dirty try/catch hack as the only solution is very lame 😦
Pretty annoying issue. I run into it with
connectOrCreate. The issue is that the parent record isn’t created either. I could just ignore the error for the related record as it’s already been added. But it causes the entire operation to fail which I can’t have 😦@felipap I discovered when the
updateis empty the native upsert is not used, even if the rest of the rules are followed. This is an odd choice (or just a bug) but you can work around it by addingdestinationId: dest.id,to the update.@garrensmith – this does not seem to be the case when using a multicolumn unique index. I have the same fields in both
whereandcreate, like so:Outputting the logs to console, it’s doing a prisma client read, then insert rather than using
ON CONFLICT. The docs say there must be only one unique field, but it doesn’t clarify whether a multicolumn unique index works as to use the native upsert. Could you clarify this?I fear that dropped between the other work. Can you open a feature request for this? Generally I see no reason why we should not also do similar changes/optimizations (if I am allowed to call them that) for MySQL 👍
Prisma Upserts are very powerful and support very nested upserts. This can lead to a Unique constraint error being thrown. We have updated our docs with why this happens and what to do in that situation. We have also added support for
INSERT .. ON CONFLICT .. UPDATEsee the docs on when it will be used.@Noor0 That case is probably better represented in https://github.com/prisma/prisma/issues/9678 @elie222 @Dremora Are you aware of any issues that spell out this problem with examples of
connectOrCreateusage?I’m having a similar issue, both with
upsertandconnectOrCreate. Prisma 2.15.0. In both cases, the key I’m using in thewhereclause is a unique index composed of two columns. I’m manually populating both values in thecreateclause (so they are not autoincremented values generated by the database). Of course in the real world the values are dynamic, but we had cases where the same set of values gets sent into our system nearly simultaneously.Here’s an example schema, code and logs (table and field names have been redacted). Even though this is
upsertonly (it’s simpler),connectOrCreategenerates similar transactions and queries, and results in the same issue.If you look at the logs, it becomes obvious what’s happening. Two transactions start nearly simultaneously, both query
itemsfor a particular set of values, both determine that a new item needs to be created. So far database operations have been able to run in parallel. Then transaction 1 tries to create a newitemin the database, transaction 2 has to wait. Once transaction 1 successfully commits, transaction 2 tries to create the same item - and fails.Interestingly enough, we had similar code before with Knex, using hand-written transactions, and this rare-to-reproduce bug was present there as well.
@guillaumeLamanda
I checked your reproduction and the output is expected. Since
idproperty is not defined in the object that you are passing, it isundefinedin javascript terms. If you passundefinedas a value to the client, it will omit that field: https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined