diff --git a/backend/matching-service/prisma/schema.prisma b/backend/matching-service/prisma/schema.prisma index 020a0b67fc..f80c526e56 100644 --- a/backend/matching-service/prisma/schema.prisma +++ b/backend/matching-service/prisma/schema.prisma @@ -8,19 +8,20 @@ generator client { } model MatchRecord { - recordId Int @id @default(autoincrement()) - userId String // ID of the user waiting for a match - topic String // Preferred topic for the match - difficulty String // Preferred difficulty level for the match - socketId String // Socket Id for communication - matched Boolean @default(false) // Whether a match has been made - matchedUserId String? // Optional field to store matched user's ID - isPending Boolean @default(false) // Whether a match is currently pending confirmation - isConfirmed Boolean @default(false) // Whether a match is confirmed by the user - isArchived Boolean @default(false) // Whether a match record has been archived - createdAt DateTime @default(now()) // Timestamp for the match attempt - roomNumber String // Matched room number - questionId Int? // Question ID for the match + recordId Int @id @default(autoincrement()) + userId String // ID of the user waiting for a match + topic String // Preferred topic for the match + difficulty String // Preferred difficulty level for the match + socketId String // Socket Id for communication + matched Boolean @default(false) // Whether a match has been made + matchedUserId String? // Optional field to store matched user's ID + isPending Boolean @default(false) // Whether a match is currently pending confirmation + isConfirmed Boolean @default(false) // Whether a match is confirmed by the user + isArchived Boolean @default(false) // Whether a match record has been archived + createdAt DateTime @default(now()) // Timestamp for the match attempt + constraintsRelaxed Boolean @default(false) + roomNumber String // Matched room number + questionId Int? // Question ID for the match } model SessionHistory { @@ -34,5 +35,5 @@ model SessionHistory { userTwoId String isUserOneActive Boolean @default(true) isUserTwoActive Boolean @default(true) - createdAt DateTime @default(now()) // Timestamp for the start of session + createdAt DateTime @default(now()) // Timestamp for the start of session } diff --git a/backend/matching-service/src/matchingService.ts b/backend/matching-service/src/matchingService.ts index 1b9159758b..943e6e7ae9 100644 --- a/backend/matching-service/src/matchingService.ts +++ b/backend/matching-service/src/matchingService.ts @@ -8,6 +8,7 @@ const prisma = new PrismaClient(); const DELAY_TIME = 30000; const CONFIRM_DELAY_TIME = 10000; +const RELAX_CONSTRAINT_DELAY = 10000; export async function handleMatchingRequest(userRequest: any, socketId: string) { userRequest.socketId = socketId; @@ -38,7 +39,129 @@ function sendConfirmDelayedTimeoutMessage(recordId: string) { console.log("Sent delayed message for confirm timeout for recordId: ", recordId); } -export async function handleUserRequest(userRequest: any) { +function sendRelaxConstraintsMessage(userRequest: any) { + userRequest["type"] = "relax_constraints"; + sendDelayedMessage(userRequest, RELAX_CONSTRAINT_DELAY); + console.log("Scheduled to relax constraints for user:", userRequest.userId); +} + +export async function handleUserRequestWithRelaxedConstraints(userRequest: any) { + const { userId, topic, difficulty } = userRequest; + + const user = await prisma.matchRecord.findFirst({ + where: { userId, isPending: false, isArchived: false }, + }); + + if (user) { + await prisma.matchRecord.update({ + where: { recordId: user.recordId }, + data: { constraintsRelaxed: true }, + }); + } else { + console.error(`No match record found for user ${userId} during relaxed constraints matching`); + return; + } + + const excludedUserIds = [userId]; + + let existingMatch = null; + + existingMatch = await prisma.matchRecord.findFirst({ + where: { + topic, + difficulty, + matched: false, + isArchived: false, + userId: { + notIn: excludedUserIds, + }, + }, + }); + + if (existingMatch === null) { + existingMatch = await prisma.matchRecord.findFirst({ + where: { + topic, + matched: false, + isArchived: false, + userId: { + notIn: excludedUserIds, + }, + }, + }); + } + + if (existingMatch !== null && existingMatch.constraintsRelaxed) { + // Proceed with matching logic + const roomNumber = uuidv4(); + const question = await fetchRandomQuestion(difficulty, topic); + + if (!question) { + io.to(userRequest.socketId).emit("question_error", { + message: "No Question found for the selected topic", + }); + io.to(existingMatch.socketId).emit("question_error", { + message: "No Question found for the selected topic", + }); + await prisma.matchRecord.delete({ + where: { recordId: existingMatch.recordId }, + }); + return; + } + + // Match found, update both records to mark as isPending + await prisma.matchRecord.update({ + where: { recordId: existingMatch.recordId }, + data: { + matched: true, + matchedUserId: userId, + isPending: true, + roomNumber, + questionId: question.questionId as number, + }, + }); + + // Update current user's match record + const currentUserRecord = await prisma.matchRecord.findFirst({ + where: { userId, isPending: false, isArchived: false }, + }); + + if (currentUserRecord) { + await prisma.matchRecord.update({ + where: { recordId: currentUserRecord.recordId }, + data: { + matched: true, + matchedUserId: existingMatch.userId, + isPending: true, + roomNumber, + questionId: question.questionId as number, + }, + }); + } else { + console.error(`No match record found for user ${userId} during relaxed constraints matching`); + return; + } + + // Update both clients about the successful match + io.to(currentUserRecord.socketId).emit("matched", { + matchedWith: existingMatch.userId, + roomNumber, + questionId: question.questionId, + }); + io.to(existingMatch.socketId).emit("matched", { + matchedWith: userId, + roomNumber, + questionId: question.questionId, + }); + + sendConfirmDelayedTimeoutMessage(currentUserRecord.recordId.toString()); + sendConfirmDelayedTimeoutMessage(existingMatch.recordId.toString()); + } else { + console.log(`No match found for ${userId} after relaxing constraints, waiting for future matches`); + } +} + +export async function handleUserRequestWithoutRelaxedConstraints(userRequest: any) { const { userId, topic, difficulty, socketId } = userRequest; // Check if user already has a match record @@ -65,37 +188,85 @@ export async function handleUserRequest(userRequest: any) { return; } - // Check if there is an existing match - const existingMatch = await prisma.matchRecord.findFirst({ + // Get list of previous matches + const previousMatches = await prisma.sessionHistory.findMany({ + where: { + OR: [{ userOneId: userId }, { userTwoId: userId }], + }, + select: { + userOneId: true, + userTwoId: true, + }, + }); + + const previousUserIds = previousMatches.flatMap((match) => { + if (match.userOneId === userId) { + return [match.userTwoId]; + } else { + return [match.userOneId]; + } + }); + + // Build list of userIds to exclude + const excludedUserIds = [userId, ...previousUserIds]; + + let existingMatch = null; + + // First attempt: same topic and difficulty, excluding previous matches + existingMatch = await prisma.matchRecord.findFirst({ where: { topic, difficulty, matched: false, isArchived: false, - NOT: { userId }, + userId: { + notIn: excludedUserIds, + }, }, }); + if (existingMatch === null) { + // Second attempt: same topic, any difficulty, excluding previous matches + existingMatch = await prisma.matchRecord.findFirst({ + where: { + topic, + matched: false, + isArchived: false, + userId: { + notIn: excludedUserIds, + }, + }, + }); + } + if (existingMatch !== null) { + // Proceed with matching logic const roomNumber = uuidv4(); const question = await fetchRandomQuestion(difficulty, topic); if (!question) { io.to(socketId).emit("question_error", { - message: "No Question found for the selected topic and difficulty", + message: "No Question found for the selected topic", }); io.to(existingMatch.socketId).emit("question_error", { - message: "No Question found for the selected topic and difficulty", + message: "No Question found for the selected topic", }); await prisma.matchRecord.delete({ where: { recordId: existingMatch.recordId }, }); return; } + // Match found, update both records to mark as isPending await prisma.matchRecord.update({ where: { recordId: existingMatch.recordId }, - data: { matched: true, matchedUserId: userId, isPending: true }, + data: { + matched: true, + matchedUserId: userId, + isPending: true, + roomNumber, + questionId: question.questionId as number, + }, }); const current = await prisma.matchRecord.create({ data: { @@ -107,11 +278,10 @@ export async function handleUserRequest(userRequest: any) { matchedUserId: existingMatch.userId, isPending: true, roomNumber, - questionId: question?.questionId as number, + questionId: question.questionId as number, }, }); - // Update both clients about the successful match io.to(socketId).emit("matched", { matchedWith: existingMatch.userId, }); @@ -123,6 +293,32 @@ export async function handleUserRequest(userRequest: any) { sendConfirmDelayedTimeoutMessage(current.recordId.toString()); sendConfirmDelayedTimeoutMessage(existingMatch.recordId.toString()); } else { + // No match found + // Add user to match record and schedule constraint relaxation + console.log(`No match found for ${userId}, added to record and scheduling constraint relaxation`); + await addOrUpdateMatchRecord(userRequest); + sendRelaxConstraintsMessage(userRequest); + } +} + +async function addOrUpdateMatchRecord(userRequest: any) { + const { userId, topic, difficulty, socketId } = userRequest; + + // Check if a matchRecord already exists for the user + const existingRecord = await prisma.matchRecord.findFirst({ + where: { userId, isArchived: false, isPending: false, matched: false }, + }); + + if (existingRecord) { + // Update the existing record's socketId if necessary + if (existingRecord.socketId !== socketId) { + await prisma.matchRecord.update({ + where: { recordId: existingRecord.recordId }, + data: { socketId }, + }); + } + } else { + // Create a new matchRecord const roomNumber = uuidv4(); await prisma.matchRecord.create({ data: { @@ -134,8 +330,6 @@ export async function handleUserRequest(userRequest: any) { roomNumber, }, }); - - console.log(`No match found for ${userId}, added to record`); } } @@ -259,7 +453,7 @@ export async function handleMatchingDecline(userRequest: any) { data: { isArchived: true }, }); - // user decline, match failed regardlessly + // user decline, match failed regardless console.log(`User ${userId} declined match`); io.to(matchedRecord.socketId).emit("other_declined", "Match not confirmed. Please try again."); await prisma.matchRecord.update({ @@ -294,10 +488,10 @@ export async function handleTimeout(userRequest: any) { export async function handleConfirmTimeout(recordId: string) { const recordIdInt = Number(recordId); const result = await prisma.matchRecord.findUnique({ - where: { recordId: recordIdInt, isArchived: false }, + where: { recordId: recordIdInt }, }); console.log(`Timeout: Confirm timeout for recordId ${recordId}`); - if (result !== null) { + if (result !== null && !result.isArchived) { if (result.isConfirmed === false) { console.log(`Timeout: Match not confirmed for recordId ${recordId} with userId ${result.userId}`); } else { @@ -317,7 +511,7 @@ export async function handleDisconnected(socketId: string) { const result = await prisma.matchRecord.findMany({ where: { socketId }, }); - if (result !== null) { + if (result && result.length > 0) { await prisma.matchRecord.updateMany({ where: { socketId }, data: { isArchived: true }, diff --git a/backend/matching-service/src/rabbitmq.ts b/backend/matching-service/src/rabbitmq.ts index a56afa621b..b4f38f573b 100644 --- a/backend/matching-service/src/rabbitmq.ts +++ b/backend/matching-service/src/rabbitmq.ts @@ -1,8 +1,9 @@ import amqp from "amqplib"; import { - handleUserRequest, + handleUserRequestWithRelaxedConstraints, handleTimeout, handleConfirmTimeout, + handleUserRequestWithoutRelaxedConstraints, } from "./matchingService"; const RABBITMQ_URL = process.env.RABBITMQ_URL || "amqp://localhost"; @@ -29,7 +30,7 @@ export async function setupRabbitMQ() { if (msg !== null) { const userRequest = JSON.parse(msg.content.toString()); console.log("Received from queue:", userRequest); - await handleUserRequest(userRequest); + await handleUserRequestWithoutRelaxedConstraints(userRequest); rabbitMQChannel.ack(msg); } }); @@ -43,8 +44,9 @@ export async function setupRabbitMQ() { await handleTimeout(userRequest); } else if (userRequest.type === "confirm_timeout") { await handleConfirmTimeout(userRequest.recordId); + } else if (userRequest.type === "relax_constraints") { + await handleUserRequestWithRelaxedConstraints(userRequest); } - rabbitMQChannel.ack(msg); } }); @@ -66,6 +68,6 @@ export function sendDelayedMessage(message: any, delay: number) { Buffer.from(JSON.stringify(message)), { headers: { "x-delay": delay }, - }, + } ); }