I’m building an anonymous chat app for learning purpose where users are matched based on gender preferences. The application allows users to go online and be matched with another online user if their gender preferences match. However, I'm encountering an issue where users can end up in circular matches involving three or more users when they go online simultaneously.
I’m using PostgreSQL with Prisma and Node.js (Express) for this application. Here’s the current setup:
Database Table:
model User {
id String @id @default(uuid())
currentChatPartner String?
gender String?
preferGender String?
lastMatch String?
currentCountry String?
ageRange String?
createdAt DateTime @default(now())
report Int @default(0)
online Boolean?
}
Matching Algorithm:
async findMatch(id: string): Promise<User[] | null> {
return await prisma.$transaction(async (tx) => {
// Lock the current user
const [user] = await tx.$queryRaw<User[]>`
SELECT * FROM "User"
WHERE "id" = ${id}
FOR UPDATE
`;
// Ensure the user exists
if (!user) throw new Error("User not found");
// Lock the potential match
const [match] = await tx.$queryRaw<User[]>`
SELECT * FROM "User"
WHERE "online" = true
AND "gender" = ${user.preferGender}
AND "id" != ${id}
AND ("lastMatch" != ${id} OR "lastMatch" IS NULL)
AND "currentChatPartner" IS NULL
FOR UPDATE
LIMIT 1
`;
if (match) {
// Check if either user is already engaged in another chat
if (user.currentChatPartner || match.currentChatPartner) {
throw new Error("User is already engaged in another chat");
}
// Update the current user
await tx.user.update({
where: { id },
data: {
lastMatch: match.id,
online: false,
currentChatPartner: match.id,
},
});
// Update the matched user
await tx.user.update({
where: { id: match.id },
data: {
lastMatch: id,
online: false,
currentChatPartner: id,
},
});
return [user, match];
} else {
// If no match found, set the user to online again
await tx.user.update({
where: { id },
data: {
online: true,
},
});
return null;
}
});
}
Issue Despite using row-level locking with FOR UPDATE, my application still encounters cases where three users are matched with each other in a circular fashion when they come online simultaneously. The FOR UPDATE lock doesn't seem to be preventing these circular matches effectively.
Questions
- Is there an issue with the current approach using FOR UPDATE for locking?
- How can I refine the matching algorithm to effectively prevent circular matches?
- Are there any additional strategies or best practices for handling this type of situation in a real-time chat application?
What I've Tried
- Used FOR UPDATE to lock rows during the match process.
- Added checks to ensure users are not already engaged in another chat.
FOR UPDATE
for aFOR UPDATE SKIP LOCKED
and you should be good. The fact that you lock the row only makes concurrent requests wait for it to be released, but they still use that same record later.SKIP LOCKED
will make them skip to a different one instead. Alternatively, you can useNOWAIT
to make them fail with an exception you can handle in your app.