FG
๐Ÿ—„๏ธ DatabasesVercel

upsert across HTTP requests has a race condition

Freshabout 21 hours ago
Mar 14, 20260 views
Confidence Score85%
85%

Problem

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. [code block] How to reproduce Expected behavior I expect it to be created once and then updated the just created object, instead of crashing. Prisma information [code block] Environment & setup - OS: Mac OS, - Database: PostgreSQL - Node.js version: v13.3.0 - Prisma version: 2.3.0 [code block]

Error Output

error occurs: Unique constraint failed on the fields.

Unverified for your environment

Select your OS to check compatibility.

1 Fix

Canonical Fix
Unverified Fix
New Fix โ€“ Awaiting Verification

Implement Optimistic Locking for Upsert Operations

Medium Risk

The race condition occurs because multiple concurrent upsert requests are trying to create or update the same object with a unique constraint. When two requests are processed simultaneously, they both attempt to create the object before either can check for its existence, leading to a unique constraint violation.

Awaiting Verification

Be the first to verify this fix

  1. 1

    Add a Locking Mechanism

    Implement a locking mechanism using a distributed lock (e.g., Redis) to ensure that only one upsert operation can proceed for a given unique string at a time.

    javascript
    const { createClient } = require('redis');
    const redisClient = createClient();
    
    async function upsertWithLock(uniqueString, data) {
      const lockKey = `lock:${uniqueString}`;
      const lock = await redisClient.set(lockKey, 'locked', 'NX', 'EX', 5);
      if (lock) {
        try {
          const existingObject = await prisma.model.findUnique({ where: { uniqueString } });
          if (existingObject) {
            return await prisma.model.update({ where: { uniqueString }, data });
          } else {
            return await prisma.model.create({ data });
          }
        } finally {
          await redisClient.del(lockKey);
        }
      } else {
        throw new Error('Resource is locked, please try again later.');
      }
    }
  2. 2

    Handle Locking Errors Gracefully

    Ensure that the application handles errors related to locking appropriately, such as retrying the operation after a short delay.

    javascript
    async function retryUpsert(uniqueString, data, retries = 3) {
      for (let i = 0; i < retries; i++) {
        try {
          return await upsertWithLock(uniqueString, data);
        } catch (error) {
          if (error.message.includes('Resource is locked')) {
            await new Promise(res => setTimeout(res, 100)); // Wait before retrying
          } else {
            throw error;
          }
        }
      }
      throw new Error('Max retries reached.');
    }
  3. 3

    Test the Locking Mechanism

    Create unit tests to simulate concurrent upsert requests and ensure that the locking mechanism prevents unique constraint violations.

    javascript
    const { expect } = require('chai');
    
    describe('Upsert with Locking', () => {
      it('should handle concurrent upserts without unique constraint violation', async () => {
        const uniqueString = 'testUnique';
        const data = { /* data object */ };
        const promises = [retryUpsert(uniqueString, data), retryUpsert(uniqueString, data)];
        await Promise.all(promises);
        const result = await prisma.model.findUnique({ where: { uniqueString } });
        expect(result).to.exist;
      });
    });
  4. 4

    Monitor and Optimize Performance

    Monitor the performance of the locking mechanism and optimize the lock duration and retry logic based on application needs.

Validation

Confirm the fix by running concurrent upsert requests and ensuring that no unique constraint violations occur. Additionally, check that the final state of the database reflects the expected updates.

Sign in to verify this fix

Environment

Submitted by

AC

Alex Chen

2450 rep

Tags

prismaormpostgresqlbug/2-confirmedkind/bugtech/enginestopic:-broken-query