Skip to content

Commit 1972092

Browse files
k1b1t0k1b1t0ncduy0303DanielJames0302songgthu
authored
Add more languages (#22)
* Add POST API for execution service * Rewrite execution service to running code only * WIP: latest changes before merging dev * Modify docker-compose.yml * Merge collaboration service and change execution-service * feat(collaboration-service-frontend): - Integrate collaboration service backend with into Code Editor panel on /practice/:id pages. - Add placeholder panels for Question, Communication, Code Execution. * fix(code-editor-panel): dynamic loading monaco editor on client * Error handling when worker can't connect to PistonAPI * Add Agora Token generator * Add CI workflow for execution service * Integrate collaboration and execution, todo: broadcast to FE, and send code from FE * Integrate code execution to collab FE * Fix rabbitMQ startup error * Add sync error output when execution dies * Fix lock file * Clean execution service * Change buildToken function * Clean execution service and add .env example * Add .env example * Update README to include installation instructions for Piston language packages on Linux/Mac and Windows * Fix camera panel * Fix error message * Add error handling when video-call-service dies * Add languages and fix executing button * feat(frontend): implement question service (#15) * add question page and forms to create, edit, and view questions * Add dummy functions to integrate with question service backend * Fix bug in public-route to redirect based on user role * Add protected route for QuestionViewPage * update pnpm-lock.yaml * Add function to auto-create admin user * Add dropdown menu for topic selection * update pnpm-lock * update docker-compose to include health checks for mongodb and kafka * update question schema and functionalities * add solution schema and seeding * wip: integrate question service backend * update schema and controllers to faciliate question status * update seed for question and solution * update schema to allow solution fetching for corresponding question and programming language * update frontend integration to match backend schema * fix eslint warning * add archived questions view and integrate restore question endpoint * fix solution fetching logic and sanitise * fix restore functionality bug * refactor: update MongoDB URI environment variable and enhance question details retrieval - Changed environment variable from MONGODB_URI to MONGO_URI in docker-compose.yml. - Updated fetchQuestionDetails method in RoomController to include archived questions in the API request. - Modified frontend components to display question status, including an "Archived" badge where applicable. - Updated API client to support fetching archived questions. * chore: update pnpm-lock.yaml * add .dockerignore * chore: update docker-compose.yml to add environment variables and service dependencies * update auto-seeding functionality for question solutions --------- Co-authored-by: Chi Thanh <[email protected]> Co-authored-by: TramMinhMan <[email protected]> * Fix dashboard's command list --------- Co-authored-by: k1b1t0 <[email protected]> Co-authored-by: Nguyen Cao Duy <[email protected]> Co-authored-by: TramMinhMan <[email protected]> Co-authored-by: songgthu <[email protected]> Co-authored-by: Chi Thanh <[email protected]>
1 parent f8efa2e commit 1972092

File tree

10 files changed

+165
-64
lines changed

10 files changed

+165
-64
lines changed

backend/collaboration-service/src/http/codeExecutionRoutes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const router = express.Router();
77
const RABBITMQ_URL = 'amqp://user:password@rabbitmq';
88
const QUEUE_NAME = 'execution_jobs';
99

10-
const EXECUTION_TIMEOUT_MS = 30000 // 30s
10+
const EXECUTION_TIMEOUT_MS = 60000 // 60s
1111
const activeTimers = new Map()
1212

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

6161
console.log(">>> Got code ", job)
62+
roomManager.broadcastToRoom(room_id, {
63+
type: "code-execution-started"
64+
})
6265

6366
// delete old timers
6467
if (activeTimers.has(room_id)) {

backend/collaboration-service/src/models/roomModel.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,21 @@ const roomSchema = new mongoose.Schema(
1818
},
1919
programmingLanguage: {
2020
type: String,
21-
enum: ["cpp", "java", "javascript", "python"],
21+
enum: [
22+
"c",
23+
"cpp",
24+
"csharp",
25+
"go",
26+
"java",
27+
"javascript",
28+
"kotlin",
29+
"php",
30+
"python",
31+
"ruby",
32+
"rust",
33+
"swift",
34+
"typescript",
35+
],
2236
default: "python",
2337
required: true,
2438
},

backend/execution-service/src/worker.js

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ const PISTON_URL = process.env.PISTON_URL
88

99
const MAX_RETRIES = 3
1010
const RETRY_DELAY_MS = 2000
11-
const PISTON_CALL_DELAY_MS = 10000
11+
const AXIOS_CONNECTION_TIMEOUT_MS = 2000
12+
const PISTON_EXECUTION_TIMEOUT_MS = 40000 // 40s
1213

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

15-
async function callPistonAPI(language, source_code) {
16+
async function callPistonAPI(language, source_code, timeout_ms) {
1617
try {
1718
const payload = {
1819
language: language,
@@ -21,10 +22,12 @@ async function callPistonAPI(language, source_code) {
2122
{
2223
content: source_code
2324
}
24-
]
25+
],
26+
run_timeout: 10000,
27+
compile_timeout: 30000
2528
}
2629
const response = await axios.post(PISTON_URL, payload, {
27-
timeout: PISTON_CALL_DELAY_MS
30+
timeout: timeout_ms
2831
})
2932
return response.data
3033
} catch (error) {
@@ -55,29 +58,13 @@ async function resultCallback(result) {
5558
* @returns result: {room_id, isError, output}
5659
*/
5760
async function processSubmission(submit) {
58-
// If fail to connect Piston API for 3 times, then the submit is failed
5961
let result = {}
6062
result.room_id = submit.room_id
61-
for (let i = 1; i <= MAX_RETRIES; i++) {
62-
try {
63-
const response = await callPistonAPI(submit.language, submit.source_code)
64-
65-
// set isError
66-
if (response.run.status) { // runtime error
67-
result.isError = true
68-
// get readable message or output
69-
result.output = response.run.message || response.run.output
70-
} else if (response.run.code !== 0) { // runcode != 0 error
71-
result.isError = true
72-
result.output = response.run.output
73-
} else { // successfully run the code
74-
result.isError = false
75-
result.output = response.run.output
76-
}
77-
7863

79-
console.log(">>> Finish job", result)
80-
return result
64+
// first 2 time: check for connection
65+
for (let i = 1; i <= MAX_RETRIES - 1; i++) {
66+
try {
67+
const response = await callPistonAPI(submit.language, submit.source_code, AXIOS_CONNECTION_TIMEOUT_MS)
8168
} catch (error) {
8269
if (error.response) {
8370
const statusCode = error.response.status
@@ -88,26 +75,62 @@ async function processSubmission(submit) {
8875
console.log(">>> Finish job (error)", result)
8976
return result
9077
} else {
91-
// rerun
78+
// piston temp error
9279
console.log(">>> Piston api temporary error: ", statusCode)
9380
}
94-
} else {
95-
// rerun
81+
} else if (axios.isAxiosError(error) && error.code !== 'ECONNREFUSED') {
82+
// timeout / no connection
9683
console.log(">>> Piston api doesn't respond\n", error.message)
84+
} else {
85+
// ECONNREFUSED
86+
console.log(">>> Piston api is dead (ECONNREFUSED)\n", error.message)
9787
}
98-
if (i === MAX_RETRIES) {
99-
break
88+
89+
// next try
90+
const delay = RETRY_DELAY_MS * i;
91+
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
92+
await sleep(delay)
93+
continue
94+
}
95+
// if no error, quit try loop
96+
break
97+
}
98+
99+
// final try: timeout 40s
100+
try {
101+
const response = await callPistonAPI(submit.language, submit.source_code, PISTON_EXECUTION_TIMEOUT_MS)
102+
103+
// set isError
104+
if (response.run.status) { // runtime error
105+
result.isError = true
106+
// get readable message or output
107+
result.output = response.run.message || response.run.output
108+
} else if (response.run.code !== 0) { // runcode != 0 error
109+
result.isError = true
110+
result.output = response.run.output
111+
} else { // successfully run the code
112+
result.isError = false
113+
result.output = response.run.output
114+
}
115+
116+
117+
console.log(">>> Finish job", result)
118+
return result
119+
} catch (error) {
120+
if (error.response) {
121+
const statusCode = error.response.status
122+
if (statusCode >= 400 && statusCode < 500) {
123+
result.isError = true
124+
result.output = error.response.data.message
125+
return result
100126
}
101127
}
102-
const delay = RETRY_DELAY_MS * i;
103-
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
104-
await sleep(delay)
128+
// failed (after 3 tries)
129+
result.isError = true
130+
result.output = "Code execution service is unavailable. Please try again later."
131+
console.log('>>> Failed job ', result)
132+
return result
105133
}
106-
// failed (after 3 tries)
107-
result.isError = true
108-
result.output = "Code execution service is unavailable. Please try again later."
109-
console.log('>>> Failed job ', result)
110-
return result
111134
}
112135

113136
async function startWorker() {

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ services:
202202
ports:
203203
- '2000:2000'
204204
privileged: true
205+
environment:
206+
- PISTON_COMPILE_TIMEOUT=30000
207+
- PISTON_RUN_TIMEOUT=10000
205208
volumes:
206209
- piston_packages:/piston/packages
207210
networks:

frontend/src/components/dashboard/active-rooms-panel.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { RoomDetails } from "@/lib/api-client";
4-
import { ProgrammingLanguage } from "@/utils/enums";
4+
import { ProgrammingLanguage, ProgrammingLanguageDisplay } from "@/utils/enums";
55
import {
66
Card,
77
CardContent,
@@ -21,10 +21,19 @@ interface ActiveRoomsPanelProps {
2121
}
2222

2323
const programmingLanguageDisplayMap: Record<ProgrammingLanguage, string> = {
24-
[ProgrammingLanguage.CPP]: "C++",
25-
[ProgrammingLanguage.JAVA]: "Java",
26-
[ProgrammingLanguage.JAVASCRIPT]: "JavaScript",
27-
[ProgrammingLanguage.PYTHON]: "Python",
24+
[ProgrammingLanguage.C]: ProgrammingLanguageDisplay.C,
25+
[ProgrammingLanguage.CPP]: ProgrammingLanguageDisplay.CPP,
26+
[ProgrammingLanguage.CSHARP]: ProgrammingLanguageDisplay.CSHARP,
27+
[ProgrammingLanguage.GO]: ProgrammingLanguageDisplay.GO,
28+
[ProgrammingLanguage.JAVA]: ProgrammingLanguageDisplay.JAVA,
29+
[ProgrammingLanguage.JAVASCRIPT]: ProgrammingLanguageDisplay.JAVASCRIPT,
30+
[ProgrammingLanguage.KOTLIN]: ProgrammingLanguageDisplay.KOTLIN,
31+
[ProgrammingLanguage.PHP]: ProgrammingLanguageDisplay.PHP,
32+
[ProgrammingLanguage.PYTHON]: ProgrammingLanguageDisplay.PYTHON,
33+
[ProgrammingLanguage.RUBY]: ProgrammingLanguageDisplay.RUBY,
34+
[ProgrammingLanguage.RUST]: ProgrammingLanguageDisplay.RUST,
35+
[ProgrammingLanguage.SWIFT]: ProgrammingLanguageDisplay.SWIFT,
36+
[ProgrammingLanguage.TYPESCRIPT]: ProgrammingLanguageDisplay.TYPESCRIPT,
2837
};
2938

3039
const formatDate = (date: Date | null): string => {

frontend/src/components/dashboard/past-rooms-panel.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useMemo, useEffect } from "react";
44
import { RoomDetails } from "@/lib/api-client";
5-
import { ProgrammingLanguage } from "@/utils/enums";
5+
import { ProgrammingLanguage, ProgrammingLanguageDisplay } from "@/utils/enums";
66
import {
77
Card,
88
CardContent,
@@ -44,10 +44,19 @@ const MOCK_TOPICS = [
4444
];
4545

4646
const programmingLanguageDisplayMap: Record<ProgrammingLanguage, string> = {
47-
[ProgrammingLanguage.CPP]: "C++",
48-
[ProgrammingLanguage.JAVA]: "Java",
49-
[ProgrammingLanguage.JAVASCRIPT]: "JavaScript",
50-
[ProgrammingLanguage.PYTHON]: "Python",
47+
[ProgrammingLanguage.C]: ProgrammingLanguageDisplay.C,
48+
[ProgrammingLanguage.CPP]: ProgrammingLanguageDisplay.CPP,
49+
[ProgrammingLanguage.CSHARP]: ProgrammingLanguageDisplay.CSHARP,
50+
[ProgrammingLanguage.GO]: ProgrammingLanguageDisplay.GO,
51+
[ProgrammingLanguage.JAVA]: ProgrammingLanguageDisplay.JAVA,
52+
[ProgrammingLanguage.JAVASCRIPT]: ProgrammingLanguageDisplay.JAVASCRIPT,
53+
[ProgrammingLanguage.KOTLIN]: ProgrammingLanguageDisplay.KOTLIN,
54+
[ProgrammingLanguage.PHP]: ProgrammingLanguageDisplay.PHP,
55+
[ProgrammingLanguage.PYTHON]: ProgrammingLanguageDisplay.PYTHON,
56+
[ProgrammingLanguage.RUBY]: ProgrammingLanguageDisplay.RUBY,
57+
[ProgrammingLanguage.RUST]: ProgrammingLanguageDisplay.RUST,
58+
[ProgrammingLanguage.SWIFT]: ProgrammingLanguageDisplay.SWIFT,
59+
[ProgrammingLanguage.TYPESCRIPT]: ProgrammingLanguageDisplay.TYPESCRIPT,
5160
};
5261

5362
const formatDate = (date: Date | null): string => {

frontend/src/components/practice/code-editor-panel.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,35 @@ import {
2020
SelectValue,
2121
} from "@/components/ui/select";
2222
import { useCollaborationState, useCollaborationActions } from "@/stores/collaboration-store";
23-
import { ProgrammingLanguage, ConnectionState } from "@/utils/enums";
23+
import { ProgrammingLanguage, ProgrammingLanguageDisplay, ConnectionState } from "@/utils/enums";
2424
import { collaborationConfig } from "@/utils/config";
2525

2626
interface CodeEditorPanelProps {
2727
readOnly?: boolean;
2828
}
2929

30-
const programmingLanguageMonacoMap: Record<ProgrammingLanguage, string> = {
31-
[ProgrammingLanguage.PYTHON]: "python",
32-
[ProgrammingLanguage.JAVASCRIPT]: "javascript",
33-
[ProgrammingLanguage.JAVA]: "java",
34-
[ProgrammingLanguage.CPP]: "cpp",
35-
};
36-
3730
const programmingLanguageDisplayMap: Record<ProgrammingLanguage, string> = {
38-
[ProgrammingLanguage.PYTHON]: "Python",
39-
[ProgrammingLanguage.JAVASCRIPT]: "JavaScript",
40-
[ProgrammingLanguage.JAVA]: "Java",
41-
[ProgrammingLanguage.CPP]: "C++",
31+
[ProgrammingLanguage.C]: ProgrammingLanguageDisplay.C,
32+
[ProgrammingLanguage.CPP]: ProgrammingLanguageDisplay.CPP,
33+
[ProgrammingLanguage.CSHARP]: ProgrammingLanguageDisplay.CSHARP,
34+
[ProgrammingLanguage.GO]: ProgrammingLanguageDisplay.GO,
35+
[ProgrammingLanguage.JAVA]: ProgrammingLanguageDisplay.JAVA,
36+
[ProgrammingLanguage.JAVASCRIPT]: ProgrammingLanguageDisplay.JAVASCRIPT,
37+
[ProgrammingLanguage.KOTLIN]: ProgrammingLanguageDisplay.KOTLIN,
38+
[ProgrammingLanguage.PHP]: ProgrammingLanguageDisplay.PHP,
39+
[ProgrammingLanguage.PYTHON]: ProgrammingLanguageDisplay.PYTHON,
40+
[ProgrammingLanguage.RUBY]: ProgrammingLanguageDisplay.RUBY,
41+
[ProgrammingLanguage.RUST]: ProgrammingLanguageDisplay.RUST,
42+
[ProgrammingLanguage.SWIFT]: ProgrammingLanguageDisplay.SWIFT,
43+
[ProgrammingLanguage.TYPESCRIPT]: ProgrammingLanguageDisplay.TYPESCRIPT,
4244
};
4345

4446
export default function CodeEditorPanel({ readOnly = false }: CodeEditorPanelProps) {
4547
const params = useParams();
4648
const roomId = params?.id as string;
4749

4850
const { roomDetails, documentContent } = useCollaborationState();
49-
const { changeLanguage, updateLanguage, setSourceCode, setExecutionResult } =
51+
const { changeLanguage, updateLanguage, setSourceCode, setExecutionResult, setIsExecuting } =
5052
useCollaborationActions();
5153

5254
const [editorInstance, setEditorInstance] = useState<editor.IStandaloneCodeEditor | null>(null);
@@ -60,7 +62,7 @@ export default function CodeEditorPanel({ readOnly = false }: CodeEditorPanelPro
6062
const bindingRef = useRef<MonacoBinding | null>(null);
6163

6264
const currentLanguage = roomDetails?.programmingLanguage || ProgrammingLanguage.PYTHON;
63-
const monacoLanguage = programmingLanguageMonacoMap[currentLanguage];
65+
const monacoLanguage = currentLanguage;
6466

6567
// Initialize Yjs and WebSocket provider for active rooms
6668
useEffect(() => {
@@ -128,6 +130,8 @@ export default function CodeEditorPanel({ readOnly = false }: CodeEditorPanelPro
128130
toast.warn("The collaboration room has been closed");
129131
} else if (message.type === "code-execution-result") {
130132
setExecutionResult(message.data);
133+
} else if (message.type === "code-execution-started") {
134+
setIsExecuting(true);
131135
}
132136
} catch {
133137
// Ignore non-JSON messages (e.g., Yjs updates)
@@ -151,6 +155,7 @@ export default function CodeEditorPanel({ readOnly = false }: CodeEditorPanelPro
151155
updateLanguage,
152156
setSourceCode,
153157
setExecutionResult,
158+
setIsExecuting,
154159
]);
155160

156161
// Update Monaco language when room language changes

frontend/src/stores/collaboration-store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { collaborationConfig } from "@/utils/config";
77
import { collaborationAPI, RoomDetails, questionAPI, Question } from "@/lib/api-client";
88

99
let executionTimer: NodeJS.Timeout | null = null;
10-
const EXECUTION_TIMEOUT_MS = 35000; // 35s
10+
const EXECUTION_TIMEOUT_MS = 60000; // 60s
1111
const VIDEO_CALL_URL = "http://localhost:8011/v1/video/";
1212
const MAX_RETRIES = 3;
1313
const RETRY_DELAY_MS = 1500;
@@ -45,6 +45,7 @@ interface CollaborationState {
4545
setSourceCode: (code: string) => void;
4646
submitCode: () => Promise<void>;
4747
setExecutionResult: (result: { output: string; isError: boolean }) => void;
48+
setIsExecuting: (isExecuting: boolean) => void;
4849

4950
fetchAgoraToken: (roomId: string, userId: string) => Promise<void>;
5051
}
@@ -187,6 +188,7 @@ export const useCollaborationStore = create<CollaborationState>()(
187188

188189
set({ isExecuting: true, executionResult: null });
189190

191+
// clear old timers if exists
190192
if (executionTimer) {
191193
clearTimeout(executionTimer);
192194
}
@@ -235,6 +237,10 @@ export const useCollaborationStore = create<CollaborationState>()(
235237
}
236238
},
237239

240+
setIsExecuting(isExecuting: boolean) {
241+
set({ isExecuting: isExecuting });
242+
},
243+
238244
// Reset state
239245
reset: () => {
240246
set({
@@ -283,6 +289,7 @@ export const useCollaborationActions = () => {
283289
const setSourceCode = useCollaborationStore((state) => state.setSourceCode);
284290
const submitCode = useCollaborationStore((state) => state.submitCode);
285291
const setExecutionResult = useCollaborationStore((state) => state.setExecutionResult);
292+
const setIsExecuting = useCollaborationStore((state) => state.setIsExecuting);
286293

287294
return {
288295
fetchRoomDetails,
@@ -294,5 +301,6 @@ export const useCollaborationActions = () => {
294301
setSourceCode,
295302
submitCode,
296303
setExecutionResult,
304+
setIsExecuting,
297305
};
298306
};

0 commit comments

Comments
 (0)