Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5dcf80c
Merge pull request #1 from CS3219-AY2526Sem1/dev
k1b1t0 Oct 9, 2025
d07e614
Add POST API for execution service
Oct 11, 2025
fef5cbe
Rewrite execution service to running code only
Oct 13, 2025
84eb792
WIP: latest changes before merging dev
Oct 16, 2025
0e9ec2b
Merge branch 'dev' into execution-service
k1b1t0 Oct 16, 2025
33d6fbb
Modify docker-compose.yml
k1b1t0 Oct 21, 2025
ecd63ef
Merge branch 'dev' into execution-service
k1b1t0 Oct 21, 2025
33b3128
Merge collaboration service and change execution-service
k1b1t0 Oct 23, 2025
5d89414
feat(collaboration-service-frontend):
ncduy0303 Oct 23, 2025
0374254
fix(code-editor-panel): dynamic loading monaco editor on client
ncduy0303 Oct 23, 2025
de9bb4e
Error handling when worker can't connect to PistonAPI
k1b1t0 Oct 24, 2025
6e2086e
Resolve confict with dev branch
k1b1t0 Oct 24, 2025
78f2dcb
Merge branch 'execution-service' into video-call-service
k1b1t0 Oct 28, 2025
0e3b49b
Add Agora Token generator
k1b1t0 Oct 28, 2025
9e445d0
Add CI workflow for execution service
k1b1t0 Oct 28, 2025
0a4cebb
Merge branch 'collaboration-service-frontend' into integrate-executio…
k1b1t0 Oct 29, 2025
01b072d
Integrate collaboration and execution, todo: broadcast to FE, and sen…
k1b1t0 Oct 29, 2025
fcb2507
Integrate code execution to collab FE
k1b1t0 Nov 5, 2025
1ab02bf
Merge remote-tracking branch 'origin' into integrate-execution-collab…
k1b1t0 Nov 5, 2025
f6befac
Fix rabbitMQ startup error
k1b1t0 Nov 5, 2025
3bf1768
Add sync error output when execution dies
k1b1t0 Nov 6, 2025
484373e
Merge video-call branch
k1b1t0 Nov 6, 2025
89c148f
Merge branch 'dev' into integrate-execution-collaboration
k1b1t0 Nov 6, 2025
e283487
Fix lock file
k1b1t0 Nov 6, 2025
77b0f6b
Clean execution service
k1b1t0 Nov 6, 2025
851da93
Change buildToken function
k1b1t0 Nov 7, 2025
22ae4cb
Clean execution service and add .env example
k1b1t0 Nov 7, 2025
aa9e19f
Add .env example
k1b1t0 Nov 7, 2025
fa7ed6f
Update README to include installation instructions for Piston languag…
DanielJames0302 Nov 7, 2025
97f9122
Fix camera panel
k1b1t0 Nov 8, 2025
1334ad9
Fix error message
k1b1t0 Nov 8, 2025
60195cb
Merge branch 'integrate-execution-collaboration' into video-call-service
k1b1t0 Nov 8, 2025
b84ca6f
Add error handling when video-call-service dies
k1b1t0 Nov 9, 2025
27e0eb5
Merge branch 'dev' into video-call-service
k1b1t0 Nov 9, 2025
e1357f4
Add languages and fix executing button
k1b1t0 Nov 10, 2025
89af4c3
Merge branch 'dev' into video-call-service
k1b1t0 Nov 10, 2025
e44d639
feat(frontend): implement question service (#15)
songgthu Nov 10, 2025
942719a
Fix dashboard's command list
k1b1t0 Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ class RoomController {
async fetchQuestionDetails(questionId) {
try {
const response = await axios.get(
`${config.QUESTION_SERVICE_URL}/v1/questions/${questionId}`,
`${config.QUESTION_SERVICE_URL}/v1/questions/${questionId}?includeArchived=true`,
{ timeout: 5000 }
);
return {
_id: response.data._id,
title: response.data.title,
difficulty: response.data.difficulty,
topic: response.data.topic,
status: response.data.status,
};
} catch (error) {
if (error.response?.status === 404) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const router = express.Router();
const RABBITMQ_URL = 'amqp://user:password@rabbitmq';
const QUEUE_NAME = 'execution_jobs';

const EXECUTION_TIMEOUT_MS = 30000 // 30s
const EXECUTION_TIMEOUT_MS = 60000 // 60s
const activeTimers = new Map()

let mqChannel = null
Expand Down Expand Up @@ -59,6 +59,9 @@ router.post("/submit-code", async (req, res) => {
}

console.log(">>> Got code ", job)
roomManager.broadcastToRoom(room_id, {
type: "code-execution-started"
})

// delete old timers
if (activeTimers.has(room_id)) {
Expand Down
16 changes: 15 additions & 1 deletion backend/collaboration-service/src/models/roomModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,21 @@ const roomSchema = new mongoose.Schema(
},
programmingLanguage: {
type: String,
enum: ["cpp", "java", "javascript", "python"],
enum: [
"c",
"cpp",
"csharp",
"go",
"java",
"javascript",
"kotlin",
"php",
"python",
"ruby",
"rust",
"swift",
"typescript",
],
default: "python",
required: true,
},
Expand Down
97 changes: 60 additions & 37 deletions backend/execution-service/src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ const PISTON_URL = process.env.PISTON_URL

const MAX_RETRIES = 3
const RETRY_DELAY_MS = 2000
const PISTON_CALL_DELAY_MS = 10000
const AXIOS_CONNECTION_TIMEOUT_MS = 2000
const PISTON_EXECUTION_TIMEOUT_MS = 40000 // 40s

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function callPistonAPI(language, source_code) {
async function callPistonAPI(language, source_code, timeout_ms) {
try {
const payload = {
language: language,
Expand All @@ -21,10 +22,12 @@ async function callPistonAPI(language, source_code) {
{
content: source_code
}
]
],
run_timeout: 10000,
compile_timeout: 30000
}
const response = await axios.post(PISTON_URL, payload, {
timeout: PISTON_CALL_DELAY_MS
timeout: timeout_ms
})
return response.data
} catch (error) {
Expand Down Expand Up @@ -55,29 +58,13 @@ async function resultCallback(result) {
* @returns result: {room_id, isError, output}
*/
async function processSubmission(submit) {
// If fail to connect Piston API for 3 times, then the submit is failed
let result = {}
result.room_id = submit.room_id
for (let i = 1; i <= MAX_RETRIES; i++) {
try {
const response = await callPistonAPI(submit.language, submit.source_code)

// set isError
if (response.run.status) { // runtime error
result.isError = true
// get readable message or output
result.output = response.run.message || response.run.output
} else if (response.run.code !== 0) { // runcode != 0 error
result.isError = true
result.output = response.run.output
} else { // successfully run the code
result.isError = false
result.output = response.run.output
}


console.log(">>> Finish job", result)
return result
// first 2 time: check for connection
for (let i = 1; i <= MAX_RETRIES - 1; i++) {
try {
const response = await callPistonAPI(submit.language, submit.source_code, AXIOS_CONNECTION_TIMEOUT_MS)
} catch (error) {
if (error.response) {
const statusCode = error.response.status
Expand All @@ -88,26 +75,62 @@ async function processSubmission(submit) {
console.log(">>> Finish job (error)", result)
return result
} else {
// rerun
// piston temp error
console.log(">>> Piston api temporary error: ", statusCode)
}
} else {
// rerun
} else if (axios.isAxiosError(error) && error.code !== 'ECONNREFUSED') {
// timeout / no connection
console.log(">>> Piston api doesn't respond\n", error.message)
} else {
// ECONNREFUSED
console.log(">>> Piston api is dead (ECONNREFUSED)\n", error.message)
}
if (i === MAX_RETRIES) {
break

// next try
const delay = RETRY_DELAY_MS * i;
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
await sleep(delay)
continue
}
// if no error, quit try loop
break
}

// final try: timeout 40s
try {
const response = await callPistonAPI(submit.language, submit.source_code, PISTON_EXECUTION_TIMEOUT_MS)

// set isError
if (response.run.status) { // runtime error
result.isError = true
// get readable message or output
result.output = response.run.message || response.run.output
} else if (response.run.code !== 0) { // runcode != 0 error
result.isError = true
result.output = response.run.output
} else { // successfully run the code
result.isError = false
result.output = response.run.output
}


console.log(">>> Finish job", result)
return result
} catch (error) {
if (error.response) {
const statusCode = error.response.status
if (statusCode >= 400 && statusCode < 500) {
result.isError = true
result.output = error.response.data.message
return result
}
}
const delay = RETRY_DELAY_MS * i;
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
await sleep(delay)
// failed (after 3 tries)
result.isError = true
result.output = "Code execution service is unavailable. Please try again later."
console.log('>>> Failed job ', result)
return result
}
// failed (after 3 tries)
result.isError = true
result.output = "Code execution service is unavailable. Please try again later."
console.log('>>> Failed job ', result)
return result
}

async function startWorker() {
Expand Down
55 changes: 52 additions & 3 deletions backend/question-service/src/config/db.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,66 @@
const mongoose = require('mongoose')
const Question = require('../models/questionModel')
const { Question, Solution } = require('../models/questionModel')
const seedData = require('../data/seed.json')
const seedSolutions = require('../data/seed-solutions.json')

const connectDB = async () => {
try {
const con = await mongoose.connect(process.env.MONGODB_URI)
const con = await mongoose.connect(process.env.MONGO_URI)
console.log(`MongoDB Connected: ${con.connection.host}`)

const questionCount = await Question.countDocuments()
if (questionCount === 0) {
console.log('Database is empty, seeding with sample questions...')
await Question.insertMany(seedData)
const toInsert = seedData.map(s => ({ ...s, status: 'Active' }))
await Question.insertMany(toInsert)
console.log(`Seeded ${seedData.length} questions`)
// sseed solutions that map to the seeded questions.
try {
const solCount = await Solution.countDocuments()
if (solCount === 0) {
console.log('No solutions found, seeding sample solutions...')

const qDocs = await Question.find({}, 'questionID title').lean()
const titleToQID = {}
qDocs.forEach(q => { if (q && q.title) titleToQID[q.title] = q.questionID })

const toInsert = seedSolutions.map(s => {
let mappedQID = s.questionID
if (!mappedQID && s.questionTitle) mappedQID = titleToQID[s.questionTitle]
if (!mappedQID) return null
return {
questionID: mappedQID,
title: s.title || `${s.questionTitle || mappedQID} - solution`,
difficulty: s.difficulty || null,
topic: s.topic || null,
language: s.language || 'JavaScript',
code: s.code || '',
explanation: s.explanation || '',
timeComplexity: s.timeComplexity || null,
spaceComplexity: s.spaceComplexity || null,
mediaLink: s.mediaLink || null,
status: s.status || 'Active'
}
}).filter(Boolean)

if (toInsert.length === 0) {
console.log('No seed solutions matched seeded questions, skipping solution auto-seed')
} else {
try {
const inserted = await Solution.insertMany(toInsert, { ordered: false })
console.log(`Seeded ${Array.isArray(inserted) ? inserted.length : (inserted && inserted.insertedCount) || 0} solutions`)
} catch (bulkErr) {
console.warn('Some errors occurred while inserting solution seeds:', bulkErr && bulkErr.message ? bulkErr.message : bulkErr)
const finalCount = await Solution.countDocuments()
console.log(`Database now contains ${finalCount} solutions`)
}
}
} else {
console.log(`Database already contains ${solCount} solutions, skipping auto-seed for solutions`)
}
} catch (err) {
console.error('Error while auto-seeding solutions:', err)
}
} else {
console.log(`Database contains ${questionCount} questions`)
}
Expand Down
Loading