From eb77d99b0448f70590c735bb568bac6b431d974f Mon Sep 17 00:00:00 2001 From: James Holcombe Date: Thu, 7 Aug 2025 12:01:42 +0100 Subject: [PATCH 1/2] temp fix to race condition --- packages/app/src/db/crud/user.ts | 45 +++++++++++++++++---------- packages/app/src/lib/auth.ts | 53 ++++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/packages/app/src/db/crud/user.ts b/packages/app/src/db/crud/user.ts index 48d92afd..6e6334f2 100644 --- a/packages/app/src/db/crud/user.ts +++ b/packages/app/src/db/crud/user.ts @@ -1,4 +1,4 @@ -import { and, eq, ilike, or } from "drizzle-orm"; +import { eq, ilike, or } from "drizzle-orm"; import { db } from "@/db"; import { User, UserInsert, user } from "@common/db/schema/user"; @@ -39,20 +39,37 @@ export async function readUserById(id: string): Promise { export async function createUser(userInsert: UserInsert): Promise { // this is done in transaction to avoid race condition when creating user, for conflicts on authId return await db.transaction(async (trx) => { - // First try to find the user by authId - const [existingUser] = await trx + // First try to find the user by authId (primary check) + const [existingUserByAuthId] = await trx .select() .from(user) - .where( - and( - eq(user.authId, userInsert.authId), - eq(user.email, userInsert.email), - ), - ) + .where(eq(user.authId, userInsert.authId)) .execute(); - if (existingUser) { - return existingUser; + if (existingUserByAuthId) { + return existingUserByAuthId; + } + + // Also check by email to handle edge cases + const [existingUserByEmail] = await trx + .select() + .from(user) + .where(eq(user.email, userInsert.email)) + .execute(); + + if (existingUserByEmail) { + // If user exists by email but not by authId, update the authId + const [updatedUser] = await trx + .update(user) + .set({ + authId: userInsert.authId, + updatedAt: new Date(), + }) + .where(eq(user.id, existingUserByEmail.id)) + .returning() + .execute(); + + return updatedUser; } // If no existing user found, create a new one @@ -60,12 +77,6 @@ export async function createUser(userInsert: UserInsert): Promise { .insert(user) .values(userInsert) .returning() - .onConflictDoUpdate({ - target: [user.email], - set: { - authId: userInsert.authId, - }, - }) .execute(); return insertedUser; diff --git a/packages/app/src/lib/auth.ts b/packages/app/src/lib/auth.ts index 90f765d9..27d17188 100644 --- a/packages/app/src/lib/auth.ts +++ b/packages/app/src/lib/auth.ts @@ -77,15 +77,50 @@ export const getServerUser = async (): Promise => { throw new Error(" New user has no email address"); } - const createdUser = await createUser({ - email, - authId, - firstName: clerkUser.firstName || "", - lastName: clerkUser.lastName || "", - picture: clerkUser.imageUrl, - privacyPolicyAcceptedAt: new Date(), - termsOfUseAcceptedAt: new Date(), - }); + // Retry mechanism for race conditions when creating new users + let createdUser: User | undefined; + let retryCount = 0; + const maxRetries = 3; + + while (retryCount < maxRetries) { + try { + createdUser = await createUser({ + email, + authId, + firstName: clerkUser.firstName || "", + lastName: clerkUser.lastName || "", + picture: clerkUser.imageUrl, + privacyPolicyAcceptedAt: new Date(), + termsOfUseAcceptedAt: new Date(), + }); + break; // Success, exit the retry loop + } catch (error: any) { + retryCount++; + + // If it's a duplicate key error and we haven't exceeded max retries, try again + if (retryCount < maxRetries) { + // Wait a bit before retrying (exponential backoff) + await new Promise((resolve) => + setTimeout(resolve, Math.pow(2, retryCount) * 100), + ); + + // Check if user was created by another request in the meantime + const existingUser = await readUserByAuthId(authId); + if (existingUser) { + createdUser = existingUser; + break; + } + continue; + } + + // If it's not a duplicate key error or we've exceeded retries, rethrow + throw error; + } + } + + if (!createdUser) { + throw new Error("Failed to create user after multiple attempts"); + } // Identify newly created user in PostHog await identifyUserServer(createdUser, { From 5a902515fdddf247d1132b6b95bd02af5df5b83f Mon Sep 17 00:00:00 2001 From: James Holcombe Date: Thu, 7 Aug 2025 12:07:07 +0100 Subject: [PATCH 2/2] push user simplification --- packages/app/src/db/crud/user.ts | 43 ++++++++++++-------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/packages/app/src/db/crud/user.ts b/packages/app/src/db/crud/user.ts index 6e6334f2..b29afacf 100644 --- a/packages/app/src/db/crud/user.ts +++ b/packages/app/src/db/crud/user.ts @@ -1,4 +1,4 @@ -import { eq, ilike, or } from "drizzle-orm"; +import { and, eq, ilike, or } from "drizzle-orm"; import { db } from "@/db"; import { User, UserInsert, user } from "@common/db/schema/user"; @@ -39,43 +39,32 @@ export async function readUserById(id: string): Promise { export async function createUser(userInsert: UserInsert): Promise { // this is done in transaction to avoid race condition when creating user, for conflicts on authId return await db.transaction(async (trx) => { - // First try to find the user by authId (primary check) - const [existingUserByAuthId] = await trx - .select() - .from(user) - .where(eq(user.authId, userInsert.authId)) - .execute(); - - if (existingUserByAuthId) { - return existingUserByAuthId; - } - // Also check by email to handle edge cases - const [existingUserByEmail] = await trx + const [existingUser] = await trx .select() .from(user) - .where(eq(user.email, userInsert.email)) + .where( + and( + eq(user.email, userInsert.email), + eq(user.authId, userInsert.authId), + ), + ) .execute(); - if (existingUserByEmail) { - // If user exists by email but not by authId, update the authId - const [updatedUser] = await trx - .update(user) - .set({ - authId: userInsert.authId, - updatedAt: new Date(), - }) - .where(eq(user.id, existingUserByEmail.id)) - .returning() - .execute(); - - return updatedUser; + if (existingUser) { + return existingUser; } // If no existing user found, create a new one const [insertedUser] = await trx .insert(user) .values(userInsert) + .onConflictDoUpdate({ + target: [user.authId], + set: { + email: userInsert.email, + }, + }) .returning() .execute();