From 67433752ea652dce1f075b63986e6da36bdbba42 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Sun, 12 Oct 2025 23:47:30 +0800 Subject: [PATCH 01/29] Fix sync when retrieving all questions --- .../src/db/model/question.ts | 11 ++- .../src/leetcode/seedBatch.ts | 81 ++++++++----------- 2 files changed, 37 insertions(+), 55 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/db/model/question.ts b/backend-services/leetcode-backend-service/src/db/model/question.ts index bda27e156..b1a15bbc4 100644 --- a/backend-services/leetcode-backend-service/src/db/model/question.ts +++ b/backend-services/leetcode-backend-service/src/db/model/question.ts @@ -42,12 +42,11 @@ const QuestionSchema = new Schema( { collection: "questions", timestamps: true }, ); -QuestionSchema.index({ - source: 1, - titleSlug: 1, - categoryTitle: 1, - difficulty: 1, -}); +// Ensure uniqueness based on titleSlug +QuestionSchema.index({ titleSlug: 1 }, { unique: true }); + +// Compound indexes for faster queries +QuestionSchema.index({ categoryTitle: 1, difficulty: 1 }); const CursorSchema = new mongoose.Schema( { diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 96bec895e..f9e1e31e3 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -38,34 +38,25 @@ export async function seedLeetCodeBatch() { const id = "questions"; const cursor = (await SeedCursor.findById(id)) ?? - new SeedCursor({ _id: id, nextSkip: 0, pageSize: 200, done: false }); - - if (cursor.done) { - return { - ok: true, - message: "Already completed.", - nextSkip: cursor.nextSkip, - done: true, - }; - } - + new SeedCursor({ _id: id, nextSkip: 0, pageSize: 200 }); const { pageSize, nextSkip } = cursor; - const { questionList, total, initial_count } = await fetchNonPaidQuestionList( + // Fetch question list using the cursor's nextSkip + const { questionList, total } = await fetchNonPaidQuestionList( pageSize, nextSkip, ); + // Check if there are no more questions to fetch if (questionList.length === 0) { - cursor.done = true; cursor.lastRunAt = new Date(); cursor.total = total ?? cursor.total; + cursor.nextSkip = total + 1; // Prevent future refetching of previously fetched items await cursor.save(); return { ok: true, - message: "No more questions. Marked done.", - nextSkip, - done: true, + message: "No more questions.", + nextSkip: cursor.nextSkip, }; } @@ -75,47 +66,40 @@ export async function seedLeetCodeBatch() { ); const ops = questionInfos.map((q) => ({ - updateOne: { - filter: { titleSlug: q.titleSlug }, - - update: { - $set: { - globalSlug: `leetcode:${q.titleSlug}`, // unique identifier - source: "leetcode", - titleSlug: q.titleSlug, - title: q.title, - - // metadata - difficulty: q.difficulty, - categoryTitle: q.categoryTitle ?? null, - timeLimit: DIFFICULTY_TIME_LIMITS[q.difficulty] ?? 60, - - // content & extras - content: q.content ?? null, - codeSnippets: q.codeSnippets ?? [], - hints: q.hints ?? [], - exampleTestcases: q.exampleTestcases ?? null, - updatedAt: new Date(), - }, - - $setOnInsert: { - createdAt: new Date(), - }, + insertOne: { + document: { + globalSlug: `leetcode:${q.titleSlug}`, // unique identifier + source: "leetcode", + titleSlug: q.titleSlug, + title: q.title, + + // metadata + difficulty: q.difficulty, + categoryTitle: q.categoryTitle ?? null, + timeLimit: DIFFICULTY_TIME_LIMITS[q.difficulty] ?? 60, + + // content & extras + content: q.content ?? null, + codeSnippets: q.codeSnippets ?? [], + hints: q.hints ?? [], + exampleTestcases: q.exampleTestcases ?? null, + createdAt: new Date(), + updatedAt: new Date(), }, - - upsert: true, }, })); const result = await Question.bulkWrite(ops, { ordered: false }); // Advance cursor - cursor.nextSkip = nextSkip + pageSize; + if (nextSkip + pageSize > total) { + // Prevent future refetching of previously fetched items + cursor.nextSkip = total + 1; + } else { + cursor.nextSkip = nextSkip + pageSize; + } cursor.lastRunAt = new Date(); cursor.total = total; - if (initial_count < pageSize) { - cursor.done = true; - } await cursor.save(); return { @@ -127,7 +111,6 @@ export async function seedLeetCodeBatch() { pageSize, nextSkip: cursor.nextSkip, total: cursor.total, - done: cursor.done, }; } From 51b8417ba5514382e073a3c31fc5db5c6a00d38b Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:11:25 +0800 Subject: [PATCH 02/29] Replace insertOne with updateOne but with $setOnInsert only --- .../src/leetcode/seedBatch.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index f9e1e31e3..11fdaadc7 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -66,26 +66,30 @@ export async function seedLeetCodeBatch() { ); const ops = questionInfos.map((q) => ({ - insertOne: { - document: { - globalSlug: `leetcode:${q.titleSlug}`, // unique identifier - source: "leetcode", - titleSlug: q.titleSlug, - title: q.title, - - // metadata - difficulty: q.difficulty, - categoryTitle: q.categoryTitle ?? null, - timeLimit: DIFFICULTY_TIME_LIMITS[q.difficulty] ?? 60, - - // content & extras - content: q.content ?? null, - codeSnippets: q.codeSnippets ?? [], - hints: q.hints ?? [], - exampleTestcases: q.exampleTestcases ?? null, - createdAt: new Date(), - updatedAt: new Date(), + updateOne: { + filter: { titleSlug: q.titleSlug }, + update: { + // setOnInsert to avoid overwriting existing entries + $setOnInsert: { + globalSlug: `leetcode:${q.titleSlug}`, + source: "leetcode", + titleSlug: q.titleSlug, + title: q.title, + + // metadata + difficulty: q.difficulty, + categoryTitle: q.categoryTitle ?? null, + timeLimit: DIFFICULTY_TIME_LIMITS[q.difficulty] ?? 60, + + // content & extras + content: q.content ?? null, + codeSnippets: q.codeSnippets ?? [], + hints: q.hints ?? [], + exampleTestcases: q.exampleTestcases ?? null, + createdAt: new Date(), + }, }, + upsert: true, }, })); From e2dd31b1af7bc7e53d942aab893864cffb9a9c95 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:35:32 +0800 Subject: [PATCH 03/29] Remove unnecessary unique constrain enforcement --- .../leetcode-backend-service/src/db/model/question.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/db/model/question.ts b/backend-services/leetcode-backend-service/src/db/model/question.ts index b1a15bbc4..46030becf 100644 --- a/backend-services/leetcode-backend-service/src/db/model/question.ts +++ b/backend-services/leetcode-backend-service/src/db/model/question.ts @@ -42,11 +42,13 @@ const QuestionSchema = new Schema( { collection: "questions", timestamps: true }, ); -// Ensure uniqueness based on titleSlug -QuestionSchema.index({ titleSlug: 1 }, { unique: true }); - // Compound indexes for faster queries -QuestionSchema.index({ categoryTitle: 1, difficulty: 1 }); +QuestionSchema.index({ + source: 1, + titleSlug: 1, + categoryTitle: 1, + difficulty: 1, +}); const CursorSchema = new mongoose.Schema( { From fd3cd12dd7087fd638c8d849f7f93c9e344f3af2 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:36:33 +0800 Subject: [PATCH 04/29] Remove initial_count from return --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 11fdaadc7..7f9e453bd 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -191,7 +191,7 @@ export async function fetchNonPaidQuestionList( const { total, questions } = res.problemsetQuestionList; const initial_count = questions.length; const questionList = questions.filter((q) => !q.isPaidOnly); - return { questionList, total, initial_count }; + return { questionList, total }; } export async function getQuestionDetail(slug: string) { From 36982edd65e84dce31223536f1aac128538bb1a6 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:39:50 +0800 Subject: [PATCH 05/29] Remove fully initial_count from fetchNonPaidQuestionList --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 7f9e453bd..142ad0979 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -176,7 +176,6 @@ export async function fetchNonPaidQuestionList( ): Promise<{ questionList: BasicInformation[]; total: number; - initial_count: number; }> { const res = await gql< QuestionList, @@ -189,7 +188,7 @@ export async function fetchNonPaidQuestionList( >(QUERY_LIST, { categorySlug: "", limit: limit, skip: skip, filters: {} }); const { total, questions } = res.problemsetQuestionList; - const initial_count = questions.length; + // const initial_count = questions.length; const questionList = questions.filter((q) => !q.isPaidOnly); return { questionList, total }; } From 8e9081e0f8fa3f795fbdc4b95b3d15b8ce59d6b9 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:47:05 +0800 Subject: [PATCH 06/29] Remove done attribute for cursor across files --- .../leetcode-backend-service/src/db/model/question.ts | 1 - .../leetcode-backend-service/src/db/types/seedBatchResponse.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/db/model/question.ts b/backend-services/leetcode-backend-service/src/db/model/question.ts index 46030becf..a36f0750a 100644 --- a/backend-services/leetcode-backend-service/src/db/model/question.ts +++ b/backend-services/leetcode-backend-service/src/db/model/question.ts @@ -55,7 +55,6 @@ const CursorSchema = new mongoose.Schema( _id: { type: String, required: true }, nextSkip: { type: Number, default: 0, index: true }, pageSize: { type: Number, default: 200 }, - done: { type: Boolean, default: false }, lastRunAt: { type: Date }, total: { type: Number, default: 0 }, }, diff --git a/backend-services/leetcode-backend-service/src/db/types/seedBatchResponse.ts b/backend-services/leetcode-backend-service/src/db/types/seedBatchResponse.ts index 47264b17a..1c82a9e07 100644 --- a/backend-services/leetcode-backend-service/src/db/types/seedBatchResponse.ts +++ b/backend-services/leetcode-backend-service/src/db/types/seedBatchResponse.ts @@ -2,7 +2,6 @@ export interface SeedBatchResponse { ok: boolean; message: string; nextSkip: number; - done: boolean; // keep room for extra fields [k: string]: unknown; } From d2a2a45c9bb42973396f377a3cd8e6696c14cf6b Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:50:26 +0800 Subject: [PATCH 07/29] Remove entirely initial_count --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 142ad0979..6951d23d1 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -188,7 +188,6 @@ export async function fetchNonPaidQuestionList( >(QUERY_LIST, { categorySlug: "", limit: limit, skip: skip, filters: {} }); const { total, questions } = res.problemsetQuestionList; - // const initial_count = questions.length; const questionList = questions.filter((q) => !q.isPaidOnly); return { questionList, total }; } From 425e37a9adc5efab718e7a67f622c76c8304e954 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 00:52:57 +0800 Subject: [PATCH 08/29] Replace magic number total + 1 with total instead --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 6951d23d1..3b45e8c58 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -48,10 +48,10 @@ export async function seedLeetCodeBatch() { ); // Check if there are no more questions to fetch - if (questionList.length === 0) { + if (questionList.length === 0 || nextSkip >= total) { cursor.lastRunAt = new Date(); cursor.total = total ?? cursor.total; - cursor.nextSkip = total + 1; // Prevent future refetching of previously fetched items + cursor.nextSkip = total; // Prevent future refetching of previously fetched items await cursor.save(); return { ok: true, @@ -98,7 +98,7 @@ export async function seedLeetCodeBatch() { // Advance cursor if (nextSkip + pageSize > total) { // Prevent future refetching of previously fetched items - cursor.nextSkip = total + 1; + cursor.nextSkip = total; } else { cursor.nextSkip = nextSkip + pageSize; } From 58bbe42f987b7c112c47198e4fd881d2f5061b7f Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Mon, 13 Oct 2025 01:06:06 +0800 Subject: [PATCH 09/29] Fix minor naming issues for comments --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 3b45e8c58..4eb778dfc 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -69,7 +69,7 @@ export async function seedLeetCodeBatch() { updateOne: { filter: { titleSlug: q.titleSlug }, update: { - // setOnInsert to avoid overwriting existing entries + // $setOnInsert to avoid overwriting existing entries $setOnInsert: { globalSlug: `leetcode:${q.titleSlug}`, source: "leetcode", From 68280a86a73f94ca9a3582eddb06461597b3c519 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Tue, 14 Oct 2025 23:17:06 +0800 Subject: [PATCH 10/29] Add question-service health check before question retrieval at seed-batch --- .../leetcode-backend-service/.env.example | 2 +- .../leetcode-backend-service/src/health.ts | 57 +++++++++++++++++++ .../src/leetcode/seedBatch.ts | 13 +++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 backend-services/leetcode-backend-service/src/health.ts diff --git a/backend-services/leetcode-backend-service/.env.example b/backend-services/leetcode-backend-service/.env.example index e8f6e28ca..891b669a0 100644 --- a/backend-services/leetcode-backend-service/.env.example +++ b/backend-services/leetcode-backend-service/.env.example @@ -1,6 +1,6 @@ MONGODB_URI=mongodb+srv://admin:@cluster3219.ll0nzkk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster3219 ADMIN_TOKEN= -QUESTION_API_URL=http://question-backend:5275/api/v1/questions +QUESTION_API_URL=http://localhost:5275/api/v1/questions # Maximum number of concurrent LeetCode detail fetches. # Optional: defaults to 6 if not set. LEETCODE_DETAIL_CONCURRENCY=6 \ No newline at end of file diff --git a/backend-services/leetcode-backend-service/src/health.ts b/backend-services/leetcode-backend-service/src/health.ts new file mode 100644 index 000000000..bccc1fddf --- /dev/null +++ b/backend-services/leetcode-backend-service/src/health.ts @@ -0,0 +1,57 @@ +import { setTimeout as delay } from "node:timers/promises"; + +type HealthOpts = { + url?: string; // health check URL + timeoutMs?: number; // per-attempt timeout + retries?: number; // number of retries after the first attempt +}; + +interface HealthResponse { + ok?: boolean; +} + +export async function checkQuestionServiceHealth({ + url = `${process.env.QUESTION_API_URL}/health`, + timeoutMs = 1500, + retries = 2, +}: HealthOpts = {}) { + if (!process.env.QUESTION_API_URL) + throw new Error("QUESTION_API_URL is not set"); + + let lastErr: unknown; + + for (let attempt = 0; attempt <= retries; attempt++) { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), timeoutMs); + + try { + const res = await fetch(url, { + method: "GET", + headers: { accept: "application/json" }, + signal: controller.signal, + }); + clearTimeout(t); + + if (!res.ok) { + lastErr = new Error(`Health endpoint returned ${res.status}`); + } else { + const body: HealthResponse = (await res.json()) as HealthResponse; + if (body?.ok !== true) { + lastErr = new Error("Health endpoint did not return ok=true"); + } else { + return true; + } + } + } catch (err) { + lastErr = err; + } + + if (attempt < retries) { + await delay(250 * 2 ** attempt); + } + } + + throw new Error( + `Question service health check failed: ${(lastErr as Error)?.message ?? lastErr}`, + ); +} diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 4eb778dfc..71690a7d7 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -11,6 +11,7 @@ import { QUERY_LIST, QUERY_DETAIL } from "./queries.js"; import type { BasicInformation, QuestionList, Details } from "./types.js"; import pLimit from "p-limit"; import { logger } from "../logger.js"; +import { checkQuestionServiceHealth } from "../health.js"; /** * Maximum number of concurrent requests for fetching question details. @@ -41,6 +42,18 @@ export async function seedLeetCodeBatch() { new SeedCursor({ _id: id, nextSkip: 0, pageSize: 200 }); const { pageSize, nextSkip } = cursor; + try { + await checkQuestionServiceHealth(); + } catch (err) { + cursor.lastRunAt = new Date(); + await cursor.save(); + return { + ok: false as const, + message: `Aborted: question service not healthy — ${(err as Error).message}`, + nextSkip: cursor.nextSkip, + }; + } + // Fetch question list using the cursor's nextSkip const { questionList, total } = await fetchNonPaidQuestionList( pageSize, From f18914ad19e0377e7e8c9262d2cc61af7f923049 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 20:03:11 +0800 Subject: [PATCH 11/29] Modify API in .env.example --- backend-services/leetcode-backend-service/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/.env.example b/backend-services/leetcode-backend-service/.env.example index 891b669a0..e67e9ca37 100644 --- a/backend-services/leetcode-backend-service/.env.example +++ b/backend-services/leetcode-backend-service/.env.example @@ -1,6 +1,6 @@ MONGODB_URI=mongodb+srv://admin:@cluster3219.ll0nzkk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster3219 ADMIN_TOKEN= -QUESTION_API_URL=http://localhost:5275/api/v1/questions +QUESTION_API_URL=http://localhost:5275/api/v1/question-service # Maximum number of concurrent LeetCode detail fetches. # Optional: defaults to 6 if not set. LEETCODE_DETAIL_CONCURRENCY=6 \ No newline at end of file From 98c127a0a82fee0ce27b5730c6059687c6db7028 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 20:12:57 +0800 Subject: [PATCH 12/29] Reformat mongo db connection for easier logging and debugging --- .../src/db/connection.ts | 21 +++++++++------- .../src/db/connection.ts | 24 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/db/connection.ts b/backend-services/leetcode-backend-service/src/db/connection.ts index 87839e84b..7d4188b6a 100644 --- a/backend-services/leetcode-backend-service/src/db/connection.ts +++ b/backend-services/leetcode-backend-service/src/db/connection.ts @@ -16,16 +16,21 @@ export default fp(async (app: FastifyInstance) => { const uri = process.env.MONGODB_URI; if (!uri) throw new Error("MONGODB_URI is missing"); - if ( - mongoose.connection.readyState === mongoose.ConnectionStates.disconnected - ) { - await mongoose.connect(uri, { - dbName: "leetcode-service", - serverSelectionTimeoutMS: 10000, - }); + if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) { + try { + await mongoose.connect(uri, { + dbName: "leetcode-service", + serverSelectionTimeoutMS: 10000, + }); + logger.info("[Mongo] Connected"); + } catch (err) { + logger.error("[Mongo] Connection failed: ", err); + throw err; + } + } else { + logger.info("[Mongo] Already connected"); } - logger.info("Mongo connected"); app.decorate("mongo", mongoose); app.addHook("onClose", async () => { diff --git a/backend-services/question-backend-service/src/db/connection.ts b/backend-services/question-backend-service/src/db/connection.ts index 5ba1edf4e..88bfe45e0 100644 --- a/backend-services/question-backend-service/src/db/connection.ts +++ b/backend-services/question-backend-service/src/db/connection.ts @@ -16,20 +16,24 @@ export default fp(async (app: FastifyInstance) => { const uri = process.env.MONGODB_URI; if (!uri) throw new Error("MONGODB_URI is missing"); - if ( - mongoose.connection.readyState === mongoose.ConnectionStates.disconnected - ) { - await mongoose.connect(uri, { - dbName: "question-service", - serverSelectionTimeoutMS: 10000, - }); + if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) { + try { + await mongoose.connect(uri, { + dbName: "question-service", + serverSelectionTimeoutMS: 10000, + }); + logger.info("[Mongo] Connected"); + } catch (err) { + logger.error("[Mongo] Connection failed: ", err); + throw err; + } + } else { + logger.info("[Mongo] Already connected"); } - - logger.info("Mongo connected"); app.decorate("mongo", mongoose); app.addHook("onClose", async () => { await mongoose.connection.close(); - logger.info("Mongo disconnected"); + logger.info("[Mongo] Disconnected"); }); }); From afcdd048def572aa7950d99400154c0f191f2b1f Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 20:14:18 +0800 Subject: [PATCH 13/29] Fix docker run command in question-backend-service README --- backend-services/question-backend-service/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-services/question-backend-service/README.md b/backend-services/question-backend-service/README.md index 48b13dbd7..becd3e3d0 100644 --- a/backend-services/question-backend-service/README.md +++ b/backend-services/question-backend-service/README.md @@ -47,7 +47,7 @@ OR ```bash docker network create peerprep_net # If not created yet docker build --tag question-service . -docker run --rm --publish 5275:5275 --env-file .env -name question-backend --network peerprep_net question-service +docker run --rm --publish 5275:5275 --env-file .env --name question-backend --network peerprep_net question-service ``` You should see logs like: From a213802cfa2daa08147773f48edb8305f03ded4c Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 23:49:59 +0800 Subject: [PATCH 14/29] Add infrastructure for failedQuestion storing --- .../src/db/changeStream.ts | 9 +++------ .../src/db/model/failedQuestion.ts | 15 +++++++++++++++ .../src/db/types/failedQuestion.ts | 8 ++++++++ .../src/leetcode/seedBatch.ts | 11 +++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts create mode 100644 backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts diff --git a/backend-services/leetcode-backend-service/src/db/changeStream.ts b/backend-services/leetcode-backend-service/src/db/changeStream.ts index 176958935..1742a714c 100644 --- a/backend-services/leetcode-backend-service/src/db/changeStream.ts +++ b/backend-services/leetcode-backend-service/src/db/changeStream.ts @@ -91,18 +91,15 @@ export default fp((app: FastifyInstance) => { const doc = change.fullDocument as QuestionDoc; if (!doc) continue; await postDoc(doc); - app.log.info({ doc }, "Got changed document:"); + logger.info("Got changed document:", { doc }); } catch (err) { - app.log.error( - { err }, - "[ChangeStream] Error processing change event", - ); + logger.error("[ChangeStream] Error processing change event", { err }); } } isProcessing = false; } changeStream.on("change", (change: ChangeStreamDocument) => { - app.log.info("[ChangeStream] Event"); + logger.info("[ChangeStream] Event"); changeQueue.push(change); void processQueue(); }); diff --git a/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts b/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts new file mode 100644 index 000000000..a1a6395b2 --- /dev/null +++ b/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts @@ -0,0 +1,15 @@ +import { Schema, model } from "mongoose"; + +const failedQuestionSchema = new Schema( + { + provider: { type: String, required: true, default: "leetcode" }, + leetcodeIndex: { type: Number, required: true }, + attempts: { type: Number, default: 0 }, + lastTriedAt: { type: Date }, + }, + { timestamps: true }, +); + +failedQuestionSchema.index({ leetcodeIndex: 1 }, { unique: true }); + +export const FailedQuestion = model("FailedQuestion", failedQuestionSchema); diff --git a/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts b/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts new file mode 100644 index 000000000..4569c216f --- /dev/null +++ b/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts @@ -0,0 +1,8 @@ +export interface FailedQuestion { + provider: string; // e.g., "leetcode" + leetcodeIndex: number; + attempts: number; // number of attempts made to fetch the question + lastTriedAt?: Date; // timestamp of the last attempt + createdAt?: Date; + updatedAt?: Date; +} diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 71690a7d7..7ef9d5055 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -60,6 +60,17 @@ export async function seedLeetCodeBatch() { nextSkip, ); + // Check if total is valid, could be undefined if fail to connect to LeetCode + if (total === undefined || total === null) { + cursor.lastRunAt = new Date(); + await cursor.save(); + return { + ok: false as const, + message: "Failed to fetch total number of questions from leetcode.", + nextSkip: cursor.nextSkip, + }; + } + // Check if there are no more questions to fetch if (questionList.length === 0 || nextSkip >= total) { cursor.lastRunAt = new Date(); From 169d6cd279fcce9730045d466ee733c617a0897e Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 23:58:00 +0800 Subject: [PATCH 15/29] Refactor fetchNonPaidQuestionInfo to reduce repetition --- .../src/leetcode/seedBatch.ts | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 7ef9d5055..6dd359680 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -87,6 +87,7 @@ export async function seedLeetCodeBatch() { const questionInfos: QuestionDetail[] = await fetchNonPaidQuestionInfo( pageSize, nextSkip, + questionList, ); const ops = questionInfos.map((q) => ({ @@ -155,35 +156,21 @@ type QuestionDetail = NonNullable; export async function fetchNonPaidQuestionInfo( limit: number, skip: number, + questionList: BasicInformation[], ): Promise { - const res = await gql< - QuestionList, - { - categorySlug: string; - limit: number; - skip: number; - filters: Record; - } - >(QUERY_LIST, { categorySlug: "", limit: limit, skip: skip, filters: {} }); - - const questionList = res.problemsetQuestionList; - const questions: BasicInformation[] = questionList.questions; - const limitConcurrency = pLimit(DETAIL_CONCURRENCY); - const tasks = questions - .filter((q) => !q.isPaidOnly) - .map((q) => - limitConcurrency(async () => { - try { - const detail = await getQuestionDetail(q.titleSlug); - return detail ?? null; - } catch { - logger.error(`Failed to fetch details for ${q.titleSlug}`); - return null; - } - }), - ); + const tasks = questionList.map((q) => + limitConcurrency(async () => { + try { + const detail = await getQuestionDetail(q.titleSlug); + return detail ?? null; + } catch { + logger.error(`Failed to fetch details for ${q.titleSlug}`); + return null; + } + }), + ); const results = await Promise.all(tasks); return results.filter((d): d is QuestionDetail => d !== null); From c6d5e9597f7e379d1d0a54a514c7a66cf9b3c4ec Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Thu, 16 Oct 2025 23:58:26 +0800 Subject: [PATCH 16/29] Fix comments --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 6dd359680..e5bb79353 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -151,6 +151,7 @@ type QuestionDetail = NonNullable; * content of paid questions will not be accessible without a premium account. * @param limit - The maximum number of questions to fetch. * @param skip - The number of questions to skip. + * @param questionList - The list of basic question information to fetch details for. * @returns An array of non-paid question details. */ export async function fetchNonPaidQuestionInfo( From 7f2a81f1c308f8aafcb4e71f0b247f9509e102e4 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 00:09:35 +0800 Subject: [PATCH 17/29] Add debug statement --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index e5bb79353..23739a4fa 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -66,7 +66,8 @@ export async function seedLeetCodeBatch() { await cursor.save(); return { ok: false as const, - message: "Failed to fetch total number of questions from leetcode.", + message: + "Failed to fetch total number of questions from leetcode. Possible failed connection to LeetCode.", nextSkip: cursor.nextSkip, }; } From 3d1a49205a8a08579a4c6b6ebe4bf0ea7256dd6a Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 00:32:40 +0800 Subject: [PATCH 18/29] Replace magic number with constant --- .../leetcode-backend-service/src/health.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/health.ts b/backend-services/leetcode-backend-service/src/health.ts index bccc1fddf..190995676 100644 --- a/backend-services/leetcode-backend-service/src/health.ts +++ b/backend-services/leetcode-backend-service/src/health.ts @@ -10,10 +10,20 @@ interface HealthResponse { ok?: boolean; } +const BATCH_HEALTH_TIMEOUT_MS = 1500; +const BATCH_HEALTH_RETRIES = 2; +const BASE_DELAY_MS = 250; + +/** Check the health of the question service. + * Throws an error if the service is unhealthy or unreachable after retries. + * @param opts - Options for health check. + * @returns A promise that resolves to true if healthy, otherwise throws an error. + */ + export async function checkQuestionServiceHealth({ url = `${process.env.QUESTION_API_URL}/health`, - timeoutMs = 1500, - retries = 2, + timeoutMs = BATCH_HEALTH_TIMEOUT_MS, + retries = BATCH_HEALTH_RETRIES, }: HealthOpts = {}) { if (!process.env.QUESTION_API_URL) throw new Error("QUESTION_API_URL is not set"); @@ -47,7 +57,7 @@ export async function checkQuestionServiceHealth({ } if (attempt < retries) { - await delay(250 * 2 ** attempt); + await delay(BASE_DELAY_MS * 2 ** attempt); } } From bf5054703a6bd983363972b8554c9f1e870d94b9 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 00:33:28 +0800 Subject: [PATCH 19/29] Remove quesion and dbSchema for failedQuestion --- .../src/db/model/failedQuestion.ts | 15 --------------- .../src/db/types/failedQuestion.ts | 8 -------- 2 files changed, 23 deletions(-) delete mode 100644 backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts delete mode 100644 backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts diff --git a/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts b/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts deleted file mode 100644 index a1a6395b2..000000000 --- a/backend-services/leetcode-backend-service/src/db/model/failedQuestion.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Schema, model } from "mongoose"; - -const failedQuestionSchema = new Schema( - { - provider: { type: String, required: true, default: "leetcode" }, - leetcodeIndex: { type: Number, required: true }, - attempts: { type: Number, default: 0 }, - lastTriedAt: { type: Date }, - }, - { timestamps: true }, -); - -failedQuestionSchema.index({ leetcodeIndex: 1 }, { unique: true }); - -export const FailedQuestion = model("FailedQuestion", failedQuestionSchema); diff --git a/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts b/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts deleted file mode 100644 index 4569c216f..000000000 --- a/backend-services/leetcode-backend-service/src/db/types/failedQuestion.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface FailedQuestion { - provider: string; // e.g., "leetcode" - leetcodeIndex: number; - attempts: number; // number of attempts made to fetch the question - lastTriedAt?: Date; // timestamp of the last attempt - createdAt?: Date; - updatedAt?: Date; -} From 3ed9d26c7fe5799ab00c230d329bf03d69514a52 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 00:37:21 +0800 Subject: [PATCH 20/29] Fix minor comment and error messages --- backend-services/leetcode-backend-service/src/health.ts | 4 +--- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/health.ts b/backend-services/leetcode-backend-service/src/health.ts index 190995676..380e6896f 100644 --- a/backend-services/leetcode-backend-service/src/health.ts +++ b/backend-services/leetcode-backend-service/src/health.ts @@ -61,7 +61,5 @@ export async function checkQuestionServiceHealth({ } } - throw new Error( - `Question service health check failed: ${(lastErr as Error)?.message ?? lastErr}`, - ); + throw new Error(`Question service health check failed: ${String(lastErr)}`); } diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 23739a4fa..a4bd70d20 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -95,7 +95,7 @@ export async function seedLeetCodeBatch() { updateOne: { filter: { titleSlug: q.titleSlug }, update: { - // $setOnInsert to avoid overwriting existing entries + // Use $setOnInsert for all fields to ensure insert-only behavior; existing entries are never updated. $setOnInsert: { globalSlug: `leetcode:${q.titleSlug}`, source: "leetcode", From 93259cd65fd59ce6e86a840ddbddb26af002ab52 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:17:41 +0800 Subject: [PATCH 21/29] Add error handling for failed leetcode connection --- .../src/leetcode/seedBatch.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index a4bd70d20..7a5dd2251 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -54,25 +54,29 @@ export async function seedLeetCodeBatch() { }; } - // Fetch question list using the cursor's nextSkip - const { questionList, total } = await fetchNonPaidQuestionList( - pageSize, - nextSkip, - ); + // Fetch question list + let questionList: BasicInformation[] = []; + let total = 0; - // Check if total is valid, could be undefined if fail to connect to LeetCode - if (total === undefined || total === null) { + try { + const { questionList: fetchedQuestionList, total: fetchedTotal } = + await fetchNonPaidQuestionList(pageSize, nextSkip); + questionList = fetchedQuestionList; + total = fetchedTotal; + } catch (err) { + logger.error( + `Failed to fetch question list from LeetCode: ${(err as Error).message}`, + ); cursor.lastRunAt = new Date(); await cursor.save(); return { - ok: false as const, - message: - "Failed to fetch total number of questions from leetcode. Possible failed connection to LeetCode.", + ok: false, + message: `Failed to fetch question list from LeetCode: ${(err as Error).message}`, nextSkip: cursor.nextSkip, }; } - // Check if there are no more questions to fetch + // Check if there are more questions to process if (questionList.length === 0 || nextSkip >= total) { cursor.lastRunAt = new Date(); cursor.total = total ?? cursor.total; @@ -200,6 +204,10 @@ export async function fetchNonPaidQuestionList( } >(QUERY_LIST, { categorySlug: "", limit: limit, skip: skip, filters: {} }); + if (!res.problemsetQuestionList) { + throw new Error("Failed to fetch question list from LeetCode"); + } + const { total, questions } = res.problemsetQuestionList; const questionList = questions.filter((q) => !q.isPaidOnly); return { questionList, total }; From 9929a2d547c73878d4021200a1642d0afbb74523 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:32:00 +0800 Subject: [PATCH 22/29] Fix README.md --- .../leetcode-backend-service/README.md | 4 +- .../question-backend-service/README.md | 107 ++++++++++-------- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/backend-services/leetcode-backend-service/README.md b/backend-services/leetcode-backend-service/README.md index bd1d707bc..98d011e9f 100644 --- a/backend-services/leetcode-backend-service/README.md +++ b/backend-services/leetcode-backend-service/README.md @@ -49,13 +49,13 @@ Server listening on http://localhost:5285 **Note**: Local development (e.g. `npm run dev`) is possible (though not recommended). To enable it, update the .env file by changing: ```bash -QUESTION_API_URL=http://question-backend:5275/api/v1/questions +QUESTION_API_URL=http://question-backend:5275/api/v1/question-service ``` to: ```bash -QUESTION_API_URL=http://localhost:5275/api/v1/questions +QUESTION_API_URL=http://localhost:5275/api/v1/question-service ``` Do change the `QUESTION_API_URL` back when using docker run. diff --git a/backend-services/question-backend-service/README.md b/backend-services/question-backend-service/README.md index becd3e3d0..27cf4ae3a 100644 --- a/backend-services/question-backend-service/README.md +++ b/backend-services/question-backend-service/README.md @@ -75,69 +75,86 @@ src/ ## API -Base URL: `http://localhost:5275/api/v1` +Base URL: `http://localhost:5275/api/v1/question-service` -### Questions — existence check +### Existence Check -**GET** `/question/exists` +**POST** `/exists-categories-difficulties` Checks whether a question with the given attributes exists. -Query params +Sample reply body: -- categoryTitle (string) -- difficulty (Easy|Medium|Hard) +```json +{ + "categories": { + "Algorithms": ["Easy", "Medium", "Hard"], + "CS": ["Easy"] + } +} +``` -Example: +Sample response: -```bash -# For window users -curl.exe http://localhost:5275/api/questions/exists?categoryTitle=Algorithms&difficulty=Easy +```json +{ + "Algorithms": { + "Easy": true, + "Medium": true, + "Hard": true + }, + "CS": { + "Easy": false + } +} ``` -### Questions — random fetch +### Fetch random question -**GET** `/question/random` +**POST** `/random` Returns a single random question filtered by query. -Query params - -- categoryTitle (string) -- difficulty (Easy|Medium|Hard) +Sample reply body: -Example: - -```bash -# For window users -curl.exe http://localhost:5275/api/questions/random?categoryTitle=Algorithms&difficulty=Easy +```json +{ + "categories": { + "Algorithms": ["Easy", "Medium", "Hard"], + "Database": ["Easy"] + } +} ``` -### Questions — insert - -**POST** `/questions/post-question` -Upserts a question document. - -## Data Model +Sample response: -`Question` (database: `question-service`) - -```ts +```json { - titleSlug: String, - title: String, - difficulty: "Easy" | "Medium" | "Hard", - categoryTitle: String, - timeLimit: Number, - content: String, - codeSnippets: [{ - lang: String, - langSlug: String, - code: String, - }], - hints: [String], - sampleTestCase: String, - createdAt: Date, - updatedAt: Date + "_id": "68ebac53b63b10de074be992", + "globalSlug": "leetcode:rearrange-products-table", + "__v": 0, + "categoryTitle": "Database", + "codeSnippets": [ + { + "lang": "MySQL", + "langSlug": "mysql", + "code": "# Write your MySQL query statement below\n" + }, + ], + "content": "

Table: Products

\n....", + "createdAt": "2025-10-12T13:25:40.139Z", + "difficulty": "Easy", + "exampleTestcases": "{\"headers\":{\"Products\":[\"product_id\",\"store1\",\"store2\",\"store3\"]},\"rows\":{\"Products\":[[0, 95, 100, 105], [1, 70, null, 80]]}}", + "hints": [], + "source": "leetcode", + "timeLimit": 30, + "title": "Rearrange Products Table", + "titleSlug": "rearrange-products-table", + "updatedAt": "2025-10-12T13:25:40.139Z" } ``` +### Insert Question into Database + +**POST** `/question` +Upserts a question document. + --- From 1259069560e6e12f48962e9762df3fbbb37063db Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:35:24 +0800 Subject: [PATCH 23/29] Fix naming issues --- .github/workflows/docker-build-push.yml | 2 +- .github/workflows/format-lint.yml | 6 +++--- .github/workflows/leetcode-update.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 412a70565..93b0176eb 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -21,7 +21,7 @@ jobs: # Add one entry per microservice - name: matching-ui context: ./ui-services/matching-ui-service - - name: questions-ui + - name: question-ui context: ./ui-services/question-ui-service - name: collab-ui context: ./ui-services/collab-ui-service diff --git a/.github/workflows/format-lint.yml b/.github/workflows/format-lint.yml index db304b101..6ef10b3af 100644 --- a/.github/workflows/format-lint.yml +++ b/.github/workflows/format-lint.yml @@ -67,7 +67,7 @@ jobs: npx eslint --format json . > eslint-report.json cat eslint-report.json | reviewdog -f=eslint -name="eslint" -reporter=github-pr-review -level=error - questions-ui-lint: + question-ui-lint: runs-on: ubuntu-latest steps: - name: Set REVIEWDOG_GITHUB_API_TOKEN @@ -100,7 +100,7 @@ jobs: eslint_exit_code=$exit_code if [ $eslint_exit_code -ne 0 ]; then - echo "ESLint failed for questions-ui" + echo "ESLint failed for question-ui" exit 1 fi @@ -180,7 +180,7 @@ jobs: eslint_exit_code=$exit_code if [ $eslint_exit_code -ne 0 ]; then - echo "ESLint failed for questions-ui" + echo "ESLint failed for question-ui" exit 1 fi diff --git a/.github/workflows/leetcode-update.yml b/.github/workflows/leetcode-update.yml index cbab04d16..fb642cb4e 100644 --- a/.github/workflows/leetcode-update.yml +++ b/.github/workflows/leetcode-update.yml @@ -48,7 +48,7 @@ jobs: run: | echo "Waiting for leetcode-backend to respond..." for i in {1..60}; do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5285/api/v1/leetcode/health || true) + code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5285/api/v1/leetcode-service/health || true) if [ "$code" = "200" ]; then echo "Leetcode backend is ready (HTTP 200)" exit 0 @@ -73,7 +73,7 @@ jobs: run: | echo "Waiting for question-backend to respond..." for i in {1..60}; do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5275/api/v1/questions/health || true) + code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5275/api/v1/question-service/health || true) if [ "$code" = "200" ]; then echo "Question backend is ready (HTTP 200)" exit 0 From de9469097778106edeef98ba79a7efee3b31419e Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:35:41 +0800 Subject: [PATCH 24/29] Fix README for leetcode-service --- backend-services/leetcode-backend-service/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend-services/leetcode-backend-service/README.md b/backend-services/leetcode-backend-service/README.md index 98d011e9f..95ef16dd1 100644 --- a/backend-services/leetcode-backend-service/README.md +++ b/backend-services/leetcode-backend-service/README.md @@ -86,11 +86,11 @@ src/ ## API -Base URL: `http://localhost:5285/api/v1` +Base URL: `http://localhost:5285/api/v1/leetcode-service` ### Seed 200 problems into Mongo -**POST** `/leetcode/seed-batch` +**POST** `/seed-batch` Fetches the next 200 problems and **upserts** to Mongo. Examples: @@ -98,7 +98,7 @@ Examples: ```bash # Replace ADMIN_TOKEN with DB password # MUSt run the question-service before running the follow command -curl.exe --request POST -H "X-Admin-Token: <>" --url "http://localhost:5285/api/v1/leetcode/seed-batch" +curl.exe --request POST -H "X-Admin-Token: " --url "http://localhost:5285/api/v1/leetcode-service/seed-batch" ``` ## Data Model From f34d83273c71777919acdd8d1cd191644a662f3c Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:38:50 +0800 Subject: [PATCH 25/29] Fix magic number issue --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 3 ++- backend-services/question-backend-service/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 7a5dd2251..5e32231d3 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -13,6 +13,7 @@ import pLimit from "p-limit"; import { logger } from "../logger.js"; import { checkQuestionServiceHealth } from "../health.js"; +const PAGE_SIZE = 200; /** * Maximum number of concurrent requests for fetching question details. * This can be configured via the LEETCODE_DETAIL_CONCURRENCY environment variable. @@ -39,7 +40,7 @@ export async function seedLeetCodeBatch() { const id = "questions"; const cursor = (await SeedCursor.findById(id)) ?? - new SeedCursor({ _id: id, nextSkip: 0, pageSize: 200 }); + new SeedCursor({ _id: id, nextSkip: 0, pageSize: PAGE_SIZE }); const { pageSize, nextSkip } = cursor; try { diff --git a/backend-services/question-backend-service/README.md b/backend-services/question-backend-service/README.md index 27cf4ae3a..03b78f468 100644 --- a/backend-services/question-backend-service/README.md +++ b/backend-services/question-backend-service/README.md @@ -137,7 +137,7 @@ Sample response: "lang": "MySQL", "langSlug": "mysql", "code": "# Write your MySQL query statement below\n" - }, + } ], "content": "

Table: Products

\n....", "createdAt": "2025-10-12T13:25:40.139Z", From cd2f45f5b4822ca65c03aa741b38b6b11162ce09 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:39:50 +0800 Subject: [PATCH 26/29] Remove redundant params --- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 5e32231d3..4dc41f216 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -90,11 +90,8 @@ export async function seedLeetCodeBatch() { }; } - const questionInfos: QuestionDetail[] = await fetchNonPaidQuestionInfo( - pageSize, - nextSkip, - questionList, - ); + const questionInfos: QuestionDetail[] = + await fetchNonPaidQuestionInfo(questionList); const ops = questionInfos.map((q) => ({ updateOne: { @@ -161,8 +158,6 @@ type QuestionDetail = NonNullable; * @returns An array of non-paid question details. */ export async function fetchNonPaidQuestionInfo( - limit: number, - skip: number, questionList: BasicInformation[], ): Promise { const limitConcurrency = pLimit(DETAIL_CONCURRENCY); From 43bffb34c47572977c5499ff2ae870a6b7aacf74 Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:51:18 +0800 Subject: [PATCH 27/29] Fix reties count and comments for setOnInsert --- backend-services/leetcode-backend-service/src/health.ts | 2 +- .../leetcode-backend-service/src/leetcode/seedBatch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend-services/leetcode-backend-service/src/health.ts b/backend-services/leetcode-backend-service/src/health.ts index 380e6896f..402997cb0 100644 --- a/backend-services/leetcode-backend-service/src/health.ts +++ b/backend-services/leetcode-backend-service/src/health.ts @@ -30,7 +30,7 @@ export async function checkQuestionServiceHealth({ let lastErr: unknown; - for (let attempt = 0; attempt <= retries; attempt++) { + for (let attempt = 0; attempt < retries; attempt++) { const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); diff --git a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts index 4dc41f216..f83b63649 100644 --- a/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts +++ b/backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts @@ -97,7 +97,7 @@ export async function seedLeetCodeBatch() { updateOne: { filter: { titleSlug: q.titleSlug }, update: { - // Use $setOnInsert for all fields to ensure insert-only behavior; existing entries are never updated. + // Use $setOnInsert for all fields to ensure insert-only behavior; existing entries' application fields are never updated (though MongoDB may update internal metadata fields). $setOnInsert: { globalSlug: `leetcode:${q.titleSlug}`, source: "leetcode", From 72d9ba50973b87910c0f0b8b281365ba492c5f2e Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:55:57 +0800 Subject: [PATCH 28/29] Add new line for .env.example --- backend-services/leetcode-backend-service/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/.env.example b/backend-services/leetcode-backend-service/.env.example index e67e9ca37..bdf8b680b 100644 --- a/backend-services/leetcode-backend-service/.env.example +++ b/backend-services/leetcode-backend-service/.env.example @@ -3,4 +3,4 @@ ADMIN_TOKEN= QUESTION_API_URL=http://localhost:5275/api/v1/question-service # Maximum number of concurrent LeetCode detail fetches. # Optional: defaults to 6 if not set. -LEETCODE_DETAIL_CONCURRENCY=6 \ No newline at end of file +LEETCODE_DETAIL_CONCURRENCY=6 From 1f16df46c833d2f76da0f792073a248f89d4d53b Mon Sep 17 00:00:00 2001 From: "sharonsohxuanhui@hotmail.com" Date: Fri, 17 Oct 2025 23:59:28 +0800 Subject: [PATCH 29/29] Fix retries issue --- backend-services/leetcode-backend-service/src/health.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend-services/leetcode-backend-service/src/health.ts b/backend-services/leetcode-backend-service/src/health.ts index 402997cb0..ee9bac39d 100644 --- a/backend-services/leetcode-backend-service/src/health.ts +++ b/backend-services/leetcode-backend-service/src/health.ts @@ -30,7 +30,8 @@ export async function checkQuestionServiceHealth({ let lastErr: unknown; - for (let attempt = 0; attempt < retries; attempt++) { + // Attempt health check with retries where retries is the number of additional attempts after the first + for (let attempt = 0; attempt < retries + 1; attempt++) { const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs);